Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: tj/commander.js
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.0.0
Choose a base ref
...
head repository: tj/commander.js
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.1.0
Choose a head ref
  • 15 commits
  • 14 files changed
  • 2 contributors

Commits on Mar 16, 2020

  1. Fix typos

    shadowspawn committed Mar 16, 2020
    Copy the full SHA
    9b0a991 View commit details
  2. Add 5.x, EOL for 4.x

    shadowspawn committed Mar 16, 2020
    Copy the full SHA
    c3895db View commit details
  3. Copy the full SHA
    2ffa6f2 View commit details
  4. Copy the full SHA
    ebc8b41 View commit details

Commits on Mar 23, 2020

  1. Fixing lint errors in TypeScript (#1208)

    * Fixing lint errors in TypeScript
    
    * Fix lint errors for constructor declarations
    
    * Disable lint error for global+namepsace
    
    * Fix new warning for space before arrow of arrow function
    shadowspawn authored Mar 23, 2020
    Copy the full SHA
    e960c90 View commit details

Commits on Mar 30, 2020

  1. Copy the full SHA
    2c0a237 View commit details
  2. Copy the full SHA
    8ec3e7f View commit details
  3. Copy the full SHA
    b5d95ee View commit details

Commits on Apr 3, 2020

  1. Copy the full SHA
    b59adfc View commit details

Commits on Apr 21, 2020

  1. Add support for multiple aliases (#1236)

    * Allow for multiple aliases #531
    
    * Add tests for multiple alias behaviours
    
    * Add aliases() mainly for getter
    
    * Fix typo
    shadowspawn authored Apr 21, 2020
    Copy the full SHA
    28e8d3f View commit details
  2. Copy the full SHA
    56221f7 View commit details
  3. Copy the full SHA
    e1966fc View commit details
  4. Copy the full SHA
    b8baafb View commit details

Commits on Apr 23, 2020

  1. Copy the full SHA
    8c9cfbb View commit details

Commits on Apr 25, 2020

  1. Copy the full SHA
    6405325 View commit details
Showing with 1,297 additions and 969 deletions.
  1. +1 −1 .github/workflows/tests.yml
  2. +30 −0 CHANGELOG.md
  3. +5 −5 Readme.md
  4. +2 −1 SECURITY.md
  5. +3 −3 examples/nestedCommands.js
  6. +94 −50 index.js
  7. +891 −797 package-lock.json
  8. +7 −7 package.json
  9. +54 −1 tests/command.alias.test.js
  10. +36 −2 tests/command.default.test.js
  11. +35 −0 tests/command.help.test.js
  12. +1 −1 tests/fixtures/pm
  13. +61 −42 typings/commander-tests.ts
  14. +77 −59 typings/index.d.ts
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [8.x, 10.x, 12.x]
node-version: [8.x, 10.x, 12.x, 14.x]
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

<!-- markdownlint-disable MD024 -->

## [5.1.0] (2020-04-25)

### Added

- support for multiple command aliases, the first of which is shown in the auto-generated help ([#531], [#1236])
- configuration support in `addCommand()` for `hidden` and `isDefault` ([#1232])

### Fixed

- omit masked help flags from the displayed help ([#645], [#1247])
- remove old short help flag when change help flags using `helpOption` ([#1248])

### Changed

- remove use of `arguments` to improve auto-generated help in editors ([#1235])
- rename `.command()` configuration `noHelp` to `hidden` (but not remove old support) ([#1232])
- improvements to documentation
- update dependencies
- update tested versions of node
- eliminate lint errors in TypeScript ([#1208])

## [5.0.0] (2020-03-14)

### Added
@@ -274,8 +295,10 @@ program
[#432]: https://github.com/tj/commander.js/issues/432
[#508]: https://github.com/tj/commander.js/issues/508
[#512]: https://github.com/tj/commander.js/issues/512
[#531]: https://github.com/tj/commander.js/issues/531
[#599]: https://github.com/tj/commander.js/issues/599
[#611]: https://github.com/tj/commander.js/issues/611
[#645]: https://github.com/tj/commander.js/issues/645
[#697]: https://github.com/tj/commander.js/issues/697
[#742]: https://github.com/tj/commander.js/issues/742
[#764]: https://github.com/tj/commander.js/issues/764
@@ -335,8 +358,15 @@ program
[#1184]: https://github.com/tj/commander.js/pull/1184
[#1191]: https://github.com/tj/commander.js/pull/1191
[#1195]: https://github.com/tj/commander.js/pull/1195
[#1208]: https://github.com/tj/commander.js/pull/1208
[#1232]: https://github.com/tj/commander.js/pull/1232
[#1235]: https://github.com/tj/commander.js/pull/1235
[#1236]: https://github.com/tj/commander.js/pull/1236
[#1247]: https://github.com/tj/commander.js/pull/1247
[#1248]: https://github.com/tj/commander.js/pull/1248

[Unreleased]: https://github.com/tj/commander.js/compare/master...develop
[5.1.0]: https://github.com/tj/commander.js/compare/v5.0.0..v5.1.0
[5.0.0]: https://github.com/tj/commander.js/compare/v4.1.1..v5.0.0
[5.0.0-4]: https://github.com/tj/commander.js/compare/v5.0.0-3..v5.0.0-4
[5.0.0-3]: https://github.com/tj/commander.js/compare/v5.0.0-2..v5.0.0-3
10 changes: 5 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -300,7 +300,7 @@ program.version('0.0.1', '-v, --vers', 'output the current version');
## Commands
You can specify (sub)commands for your top-level command using `.command()` or `.addCommand()`. There are two ways these can be implemented: using an action handler attached to the command, or as a stand-alone executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)).
You can specify (sub)commands using `.command()` or `.addCommand()`. There are two ways these can be implemented: using an action handler attached to the command, or as a stand-alone executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)).
In the first parameter to `.command()` you specify the command name and any command arguments. The arguments may be `<required>` or `[optional]`, and the last argument may also be `variadic...`.
@@ -319,22 +319,22 @@ program
});
// Command implemented using stand-alone executable file (description is second parameter to `.command`)
// Returns top-level command for adding more commands.
// Returns `this` for adding more commands.
program
.command('start <service>', 'start named service')
.command('stop [service]', 'stop named service, or all if no name supplied');
// Command prepared separately.
// Returns top-level command for adding more commands.
// Returns `this` for adding more commands.
program
.addCommand(build.makeBuildCommand());
```
Configuration options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified ([example](./examples/defaultCommand.js)).
Configuration options can be passed with the call to `.command()` and `.addCommand()`. Specifying `true` for `opts.hidden` will remove the command from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified ([example](./examples/defaultCommand.js)).
### Specify the argument syntax
You use `.arguments` to specify the arguments for the top-level command, and for subcommands they are included in the `.command` call. Angled brackets (e.g. `<required>`) indicate required input. Square brackets (e.g. `[optional]`) indicate optional input.
You use `.arguments` to specify the arguments for the top-level command, and for subcommands they are usually included in the `.command` call. Angled brackets (e.g. `<required>`) indicate required input. Square brackets (e.g. `[optional]`) indicate optional input.
```js
const { program } = require('commander');
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@ Old versions receive security updates for six months.

| Version | Supported |
| ------- | ------------------------------------------ |
| 4.x | :white_check_mark: |
| 5.x | :white_check_mark: |
| 4.x | :white_check_mark: support ends 2020-09-14 |
| 3.x | :white_check_mark: support ends 2020-05-01 |
| < 3 | :x: |

6 changes: 3 additions & 3 deletions examples/nestedCommands.js
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ const program = new commander.Command();

// Commander supports nested subcommands.
// .command() can add a subcommand with an action handler or an executable.
// .addCommand() adds a prepared command with an actiomn handler.
// .addCommand() adds a prepared command with an action handler.

// Example output:
//
@@ -21,9 +21,9 @@ brew
console.log('brew tea');
});
brew
.command('tea')
.command('coffee')
.action(() => {
console.log('brew tea');
console.log('brew coffee');
});

// Add nested commands using `.addCommand().
144 changes: 94 additions & 50 deletions index.js
Original file line number Diff line number Diff line change
@@ -117,9 +117,9 @@ class Command extends EventEmitter {
this._executableFile = null; // custom name for executable
this._defaultCommandName = null;
this._exitCallback = null;
this._alias = null;
this._aliases = [];

this._noHelp = false;
this._hidden = false;
this._helpFlags = '-h, --help';
this._helpDescription = 'display help for command';
this._helpShortFlag = '-h';
@@ -153,7 +153,7 @@ class Command extends EventEmitter {
* @param {string} nameAndArgs - command name and arguments, args are `<required>` or `[optional]` and last may also be `variadic...`
* @param {Object|string} [actionOptsOrExecDesc] - configuration options (for action), or description (for executable)
* @param {Object} [execOpts] - configuration options (for executable)
* @return {Command} returns new command for action handler, or top-level command for executable command
* @return {Command} returns new command for action handler, or `this` for executable command
* @api public
*/

@@ -174,7 +174,7 @@ class Command extends EventEmitter {
}
if (opts.isDefault) this._defaultCommandName = cmd._name;

cmd._noHelp = !!opts.noHelp;
cmd._hidden = !!(opts.noHelp || opts.hidden);
cmd._helpFlags = this._helpFlags;
cmd._helpDescription = this._helpDescription;
cmd._helpShortFlag = this._helpShortFlag;
@@ -216,11 +216,12 @@ class Command extends EventEmitter {
* See .command() for creating an attached subcommand which inherits settings from its parent.
*
* @param {Command} cmd - new subcommand
* @return {Command} parent command for chaining
* @param {Object} [opts] - configuration options
* @return {Command} `this` command for chaining
* @api public
*/

addCommand(cmd) {
addCommand(cmd, opts) {
if (!cmd._name) throw new Error('Command passed to .addCommand() must have a name');

// To keep things simple, block automatic name generation for deeply nested executables.
@@ -235,13 +236,17 @@ class Command extends EventEmitter {
}
checkExplicitNames(cmd.commands);

opts = opts || {};
if (opts.isDefault) this._defaultCommandName = cmd._name;
if (opts.noHelp || opts.hidden) cmd._hidden = true; // modifying passed command due to existing implementation

this.commands.push(cmd);
cmd.parent = this;
return this;
};

/**
* Define argument syntax for the top-level command.
* Define argument syntax for the command.
*
* @api public
*/
@@ -257,7 +262,7 @@ class Command extends EventEmitter {
* addHelpCommand(false); // force off
* addHelpCommand('help [cmd]', 'display help for [cmd]'); // force on with custom detais
*
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -293,7 +298,7 @@ class Command extends EventEmitter {
* For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
*
* @param {Array} args
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api private
*/

@@ -336,7 +341,7 @@ class Command extends EventEmitter {
* Register callback to use as replacement for calling process.exit.
*
* @param {Function} [fn] optional callback which will be passed a CommanderError, defaults to throwing
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -386,7 +391,7 @@ class Command extends EventEmitter {
* });
*
* @param {Function} fn
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -425,7 +430,7 @@ class Command extends EventEmitter {
* @param {string} description
* @param {Function|*} [fn] - custom option processing function or default vaue
* @param {*} [defaultValue]
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api private
*/

@@ -543,7 +548,7 @@ class Command extends EventEmitter {
* @param {string} description
* @param {Function|*} [fn] - custom option processing function or default vaue
* @param {*} [defaultValue]
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -561,7 +566,7 @@ class Command extends EventEmitter {
* @param {string} description
* @param {Function|*} [fn] - custom option processing function or default vaue
* @param {*} [defaultValue]
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -577,7 +582,7 @@ class Command extends EventEmitter {
* @api public
*/
allowUnknownOption(arg) {
this._allowUnknownOption = arguments.length === 0 || arg;
this._allowUnknownOption = (arg === undefined) || arg;
return this;
};

@@ -586,7 +591,7 @@ class Command extends EventEmitter {
* or store separately (specify false). In both cases the option values can be accessed using .opts().
*
* @param {boolean} value
* @return {Command} Command for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -603,7 +608,7 @@ class Command extends EventEmitter {
* or just the options (specify false).
*
* @param {boolean} value
* @return {Command} Command for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -658,7 +663,7 @@ class Command extends EventEmitter {
* @param {string[]} [argv] - optional, defaults to process.argv
* @param {Object} [parseOptions] - optionally specify style of options with from: node/user/electron
* @param {string} [parseOptions.from] - where the args are from: 'node', 'user', 'electron'
* @return {Command} for chaining
* @return {Command} `this` command for chaining
* @api public
*/

@@ -932,7 +937,7 @@ class Command extends EventEmitter {
*/
_findCommand(name) {
if (!name) return undefined;
return this.commands.find(cmd => cmd._name === name || cmd._alias === name);
return this.commands.find(cmd => cmd._name === name || cmd._aliases.includes(name));
};

/**
@@ -1172,12 +1177,12 @@ class Command extends EventEmitter {
* @param {string} str
* @param {string} [flags]
* @param {string} [description]
* @return {Command | string} this for chaining
* @return {this | string} `this` command for chaining, or version string if no arguments
* @api public
*/

version(str, flags, description) {
if (arguments.length === 0) return this._version;
if (str === undefined) return this._version;
this._version = str;
flags = flags || '-V, --version';
description = description || 'output the version number';
@@ -1196,36 +1201,57 @@ class Command extends EventEmitter {
*
* @param {string} str
* @param {Object} [argsDescription]
* @return {String|Command}
* @return {string|Command}
* @api public
*/

description(str, argsDescription) {
if (arguments.length === 0) return this._description;
if (str === undefined && argsDescription === undefined) return this._description;
this._description = str;
this._argsDescription = argsDescription;
return this;
};

/**
* Set an alias for the command
* Set an alias for the command.
*
* @param {string} alias
* @return {String|Command}
* You may call more than once to add multiple aliases. Only the first alias is shown in the auto-generated help.
*
* @param {string} [alias]
* @return {string|Command}
* @api public
*/

alias(alias) {
if (alias === undefined) return this._aliases[0]; // just return first, for backwards compatibility

let command = this;
if (this.commands.length !== 0) {
if (this.commands.length !== 0 && this.commands[this.commands.length - 1]._executableHandler) {
// assume adding alias for last added executable subcommand, rather than this
command = this.commands[this.commands.length - 1];
}

if (arguments.length === 0) return command._alias;

if (alias === command._name) throw new Error('Command alias can\'t be the same as its name');

command._alias = alias;
command._aliases.push(alias);
return this;
};

/**
* Set aliases for the command.
*
* Only the first alias is shown in the auto-generated help.
*
* @param {string[]} [aliases]
* @return {string[]|Command}
* @api public
*/

aliases(aliases) {
// Getter for the array of aliases is the main reason for having aliases() in addition to alias().
if (aliases === undefined) return this._aliases;

aliases.forEach((alias) => this.alias(alias));
return this;
};

@@ -1238,17 +1264,18 @@ class Command extends EventEmitter {
*/

usage(str) {
const args = this._args.map((arg) => {
return humanReadableArgName(arg);
});
if (str === undefined) {
if (this._usage) return this._usage;

const usage = '[options]' +
(this.commands.length ? ' [command]' : '') +
(this._args.length ? ' ' + args.join(' ') : '');
const args = this._args.map((arg) => {
return humanReadableArgName(arg);
});
return '[options]' +
(this.commands.length ? ' [command]' : '') +
(this._args.length ? ' ' + args.join(' ') : '');
}

if (arguments.length === 0) return this._usage || usage;
this._usage = str;

return this;
};

@@ -1261,7 +1288,7 @@ class Command extends EventEmitter {
*/

name(str) {
if (arguments.length === 0) return this._name;
if (str === undefined) return this._name;
this._name = str;
return this;
};
@@ -1275,15 +1302,15 @@ class Command extends EventEmitter {

prepareCommands() {
const commandDetails = this.commands.filter((cmd) => {
return !cmd._noHelp;
return !cmd._hidden;
}).map((cmd) => {
const args = cmd._args.map((arg) => {
return humanReadableArgName(arg);
}).join(' ');

return [
cmd._name +
(cmd._alias ? '|' + cmd._alias : '') +
(cmd._aliases[0] ? '|' + cmd._aliases[0] : '') +
(cmd.options.length ? ' [options]' : '') +
(args ? ' ' + args : ''),
cmd._description
@@ -1374,17 +1401,33 @@ class Command extends EventEmitter {

optionHelp() {
const width = this.padWidth();

const columns = process.stdout.columns || 80;
const descriptionWidth = columns - width - 4;
function padOptionDetails(flags, description) {
return pad(flags, width) + ' ' + optionalWrap(description, descriptionWidth, width + 2);
};

// Append the help information
return this.options.map((option) => {
// Explicit options (including version)
const help = this.options.map((option) => {
const fullDesc = option.description +
((!option.negate && option.defaultValue !== undefined) ? ' (default: ' + JSON.stringify(option.defaultValue) + ')' : '');
return pad(option.flags, width) + ' ' + optionalWrap(fullDesc, descriptionWidth, width + 2);
}).concat([pad(this._helpFlags, width) + ' ' + optionalWrap(this._helpDescription, descriptionWidth, width + 2)])
.join('\n');
return padOptionDetails(option.flags, fullDesc);
});

// Implicit help
const showShortHelpFlag = this._helpShortFlag && !this._findOption(this._helpShortFlag);
const showLongHelpFlag = !this._findOption(this._helpLongFlag);
if (showShortHelpFlag || showLongHelpFlag) {
let helpFlags = this._helpFlags;
if (!showShortHelpFlag) {
helpFlags = this._helpLongFlag;
} else if (!showLongHelpFlag) {
helpFlags = this._helpShortFlag;
}
help.push(padOptionDetails(helpFlags, this._helpDescription));
}

return help.join('\n');
};

/**
@@ -1443,8 +1486,8 @@ class Command extends EventEmitter {
}

let cmdName = this._name;
if (this._alias) {
cmdName = cmdName + '|' + this._alias;
if (this._aliases[0]) {
cmdName = cmdName + '|' + this._aliases[0];
}
let parentCmdNames = '';
for (let parentCmd = this.parent; parentCmd; parentCmd = parentCmd.parent) {
@@ -1501,7 +1544,7 @@ class Command extends EventEmitter {
*
* @param {string} [flags]
* @param {string} [description]
* @return {Command}
* @return {Command} `this` command for chaining
* @api public
*/

@@ -1511,6 +1554,7 @@ class Command extends EventEmitter {

const splitFlags = this._helpFlags.split(/[ ,|]+/);

this._helpShortFlag = undefined;
if (splitFlags.length > 1) this._helpShortFlag = splitFlags.shift();

this._helpLongFlag = splitFlags.shift();
1,688 changes: 891 additions & 797 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "commander",
"version": "5.0.0",
"version": "5.1.0",
"description": "the complete solution for node.js command-line programs",
"keywords": [
"commander",
@@ -31,14 +31,14 @@
],
"dependencies": {},
"devDependencies": {
"@types/jest": "^25.1.4",
"@types/node": "^12.12.26",
"@typescript-eslint/eslint-plugin": "^2.23.0",
"@types/jest": "^25.2.1",
"@types/node": "^12.12.36",
"@typescript-eslint/eslint-plugin": "^2.29.0",
"eslint": "^6.8.0",
"eslint-config-standard-with-typescript": "^14.0.0",
"eslint-config-standard-with-typescript": "^15.0.1",
"eslint-plugin-jest": "^23.8.2",
"jest": "^25.1.0",
"standard": "^14.3.1",
"jest": "^25.4.0",
"standard": "^14.3.3",
"typescript": "^3.7.5"
},
"typings": "typings/index.d.ts",
55 changes: 54 additions & 1 deletion tests/command.alias.test.js
Original file line number Diff line number Diff line change
@@ -12,11 +12,64 @@ test('when command has alias then appears in help', () => {
expect(helpInformation).toMatch('info|i');
});

test('when command = alias then error', () => {
test('when command has aliases added separately then only first appears in help', () => {
const program = new commander.Command();
program
.command('list [thing]')
.alias('ls')
.alias('dir');
const helpInformation = program.helpInformation();
expect(helpInformation).toMatch('list|ls ');
});

test('when command has aliases then only first appears in help', () => {
const program = new commander.Command();
program
.command('list [thing]')
.aliases(['ls', 'dir']);
const helpInformation = program.helpInformation();
expect(helpInformation).toMatch('list|ls ');
});

test('when command name = alias then error', () => {
const program = new commander.Command();
expect(() => {
program
.command('fail')
.alias('fail');
}).toThrow("Command alias can't be the same as its name");
});

test('when use alias then action handler called', () => {
const program = new commander.Command();
const actionMock = jest.fn();
program
.command('list')
.alias('ls')
.action(actionMock);
program.parse(['ls'], { from: 'user' });
expect(actionMock).toHaveBeenCalled();
});

test('when use second alias added separately then action handler called', () => {
const program = new commander.Command();
const actionMock = jest.fn();
program
.command('list')
.alias('ls')
.alias('dir')
.action(actionMock);
program.parse(['dir'], { from: 'user' });
expect(actionMock).toHaveBeenCalled();
});

test('when use second of aliases then action handler called', () => {
const program = new commander.Command();
const actionMock = jest.fn();
program
.command('list')
.aliases(['ls', 'dir'])
.action(actionMock);
program.parse(['dir'], { from: 'user' });
expect(actionMock).toHaveBeenCalled();
});
38 changes: 36 additions & 2 deletions tests/command.default.test.js
Original file line number Diff line number Diff line change
@@ -44,13 +44,47 @@ describe('default action command', () => {
expect(actionMock).toHaveBeenCalled();
});

test('when default subcommand and unrecognised argument then call default with argument', () => {
test('when default subcommand and unrecognised argument then call default', () => {
const { program, actionMock } = makeProgram();
program.parse('node test.js an-argument'.split(' '));
expect(actionMock).toHaveBeenCalled();
});

test('when default subcommand and unrecognised option then call default with option', () => {
test('when default subcommand and unrecognised option then call default', () => {
const { program, actionMock } = makeProgram();
program.parse('node test.js --an-option'.split(' '));
expect(actionMock).toHaveBeenCalled();
});
});

describe('default added command', () => {
function makeProgram() {
const actionMock = jest.fn();
const defaultCmd = new commander.Command('default')
.allowUnknownOption()
.action(actionMock);

const program = new commander.Command();
program
.command('other');
program
.addCommand(defaultCmd, { isDefault: true });
return { program, actionMock };
}

test('when default subcommand and no command then call default', () => {
const { program, actionMock } = makeProgram();
program.parse('node test.js'.split(' '));
expect(actionMock).toHaveBeenCalled();
});

test('when default subcommand and unrecognised argument then call default', () => {
const { program, actionMock } = makeProgram();
program.parse('node test.js an-argument'.split(' '));
expect(actionMock).toHaveBeenCalled();
});

test('when default subcommand and unrecognised option then call default', () => {
const { program, actionMock } = makeProgram();
program.parse('node test.js --an-option'.split(' '));
expect(actionMock).toHaveBeenCalled();
35 changes: 35 additions & 0 deletions tests/command.help.test.js
Original file line number Diff line number Diff line change
@@ -88,10 +88,45 @@ test('when call outputHelp(cb) then display cb output', () => {
writeSpy.mockClear();
});

// noHelp is now named hidden, not officially deprecated yet
test('when command sets noHelp then not displayed in helpInformation', () => {
const program = new commander.Command();
program
.command('secret', 'secret description', { noHelp: true });
const helpInformation = program.helpInformation();
expect(helpInformation).not.toMatch('secret');
});

test('when command sets hidden then not displayed in helpInformation', () => {
const program = new commander.Command();
program
.command('secret', 'secret description', { hidden: true });
const helpInformation = program.helpInformation();
expect(helpInformation).not.toMatch('secret');
});

test('when addCommand with hidden:true then not displayed in helpInformation', () => {
const secretCmd = new commander.Command('secret');

const program = new commander.Command();
program
.addCommand(secretCmd, { hidden: true });
const helpInformation = program.helpInformation();
expect(helpInformation).not.toMatch('secret');
});

test('when help short flag masked then not displayed in helpInformation', () => {
const program = new commander.Command();
program
.option('-h, --host', 'select host');
const helpInformation = program.helpInformation();
expect(helpInformation).not.toMatch(/\W-h\W.*display help/);
});

test('when both help flags masked then not displayed in helpInformation', () => {
const program = new commander.Command();
program
.option('-h, --help', 'custom');
const helpInformation = program.helpInformation();
expect(helpInformation).not.toMatch('display help');
});
2 changes: 1 addition & 1 deletion tests/fixtures/pm
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ program
.command('list', 'list packages installed').alias('lst')
.command('listen', 'listen for supported signal events').alias('l')
.command('publish', 'publish or update package').alias('p')
.command('default', 'default command', { noHelp: true, isDefault: true })
.command('default', 'default command', { hidden: true, isDefault: true })
.command('specifyInstall', 'specify install subcommand', { executableFile: 'pm-install' })
.command('specifyPublish', 'specify publish subcommand', { executableFile: 'pm-publish' })
.command('silent', 'silently succeed')
103 changes: 61 additions & 42 deletions typings/commander-tests.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import * as commander from './index';
import { Script } from 'vm';

// Test Commander usage with TypeScript.
// This is NOT a usable program, just used to test for compile errors!

// We declare lots of variables just to check types of right-side of expression, so disable this:
/* eslint-disable @typescript-eslint/no-unused-vars */

// This conflicts with esline rule saying no space!
/* eslint-disable @typescript-eslint/space-before-function-paren */

// Defined stricter type, as the options as properties `[key: string]: any`
// makes the type checking very weak.
// makes the type checking very weak.
// https://github.com/Microsoft/TypeScript/issues/25987#issuecomment-441224690
type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
@@ -28,6 +33,7 @@ const errorInstance = new commander.CommanderError(1, 'code', 'message');

// Command properties
console.log(programWithOptions.someOption);
// eslint-disable-next-line dot-notation
console.log(programWithOptions['someOption']);
const theArgs = program.args;
const theCommands: commander.Command[] = program.commands;
@@ -39,22 +45,22 @@ const versionThis3: commander.Command = program.version('1.2.3', '-r,--revision'

// command (and CommandOptions)
const commandNew1: commander.Command = program.command('action');
const commandNew2: commander.Command = program.command('action', { isDefault: true, noHelp: true });
const commandNew2: commander.Command = program.command('action', { isDefault: true, hidden: true, noHelp: true });
const commandThis1: commander.Command = program.command('exec', 'exec description');
const commandThis2: commander.Command = program.command('exec', 'exec description', { isDefault: true, noHelp: true, executableFile: 'foo' });
const commandThis2: commander.Command = program.command('exec', 'exec description', { isDefault: true, hidden: true, noHelp: true, executableFile: 'foo' });

// addCommand
const addCommandThis: commander.Command = program.addCommand(new commander.Command('abc'));

// arguments
const argumentsThis: commander.Command = program.arguments('<cmd> [env]')
const argumentsThis: commander.Command = program.arguments('<cmd> [env]');

// exitOverride
const exitThis1: commander.Command = program.exitOverride();
const exitThis2: commander.Command = program.exitOverride((err):never => {
const exitThis2: commander.Command = program.exitOverride((err): never => {
return process.exit(err.exitCode);
});
const exitThis3: commander.Command = program.exitOverride((err):void => {
const exitThis3: commander.Command = program.exitOverride((err): void => {
if (err.code !== 'commander.executeSubCommandAsync') {
throw err;
} else {
@@ -63,57 +69,61 @@ const exitThis3: commander.Command = program.exitOverride((err):void => {
});

// action
const actionThis1: commander.Command = program.action(() => { });
const actionThis2: commander.Command = program.action(async () => { });
const actionThis1: commander.Command = program.action(() => {
// do nothing.
});
const actionThis2: commander.Command = program.action(async() => {
// do nothing.
});

// option
const optionThis1: commander.Command = program.option('-a,--alpha');
const optionThis2: commander.Command = program.option('-p, --peppers', 'Add peppers')
const optionThis3: commander.Command = program.option('-s, --string [value]', 'default string', 'value')
const optionThis2: commander.Command = program.option('-p, --peppers', 'Add peppers');
const optionThis3: commander.Command = program.option('-s, --string [value]', 'default string', 'value');
const optionThis4: commander.Command = program.option('-b, --boolean', 'default boolean', false);

// example coercion functions from README

function range(val: string) {
function range(val: string): Number[] {
return val.split('..').map(Number);
}

function myParseInt(value: string, dummyPrevious: number) {
function myParseInt(value: string, dummyPrevious: number): number {
return parseInt(value);
}

function increaseVerbosity(dummyValue: string, previous: number) {
function increaseVerbosity(dummyValue: string, previous: number): number {
return previous + 1;
}

function collect(value: string, previous: string[]) {
function collect(value: string, previous: string[]): string[] {
return previous.concat([value]);
}

function commaSeparatedList(value: string, dummyPrevious: string[]) {
function commaSeparatedList(value: string, dummyPrevious: string[]): string[] {
return value.split(',');
}

const optionThis5: commander.Command = program.option('-f, --float <number>', 'float argument', parseFloat)
const optionThis6: commander.Command = program.option('-f, --float <number>', 'float argument', parseFloat, 3.2)
const optionThis7: commander.Command = program.option('-i, --integer <number>', 'integer argument', myParseInt)
const optionThis8: commander.Command = program.option('-i, --integer <number>', 'integer argument', myParseInt, 5)
const optionThis9: commander.Command = program.option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0)
const optionThis10: commander.Command = program.option('-c, --collect <value>', 'repeatable value', collect, [])
const optionThis5: commander.Command = program.option('-f, --float <number>', 'float argument', parseFloat);
const optionThis6: commander.Command = program.option('-f, --float <number>', 'float argument', parseFloat, 3.2);
const optionThis7: commander.Command = program.option('-i, --integer <number>', 'integer argument', myParseInt);
const optionThis8: commander.Command = program.option('-i, --integer <number>', 'integer argument', myParseInt, 5);
const optionThis9: commander.Command = program.option('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0);
const optionThis10: commander.Command = program.option('-c, --collect <value>', 'repeatable value', collect, []);
const optionThis11: commander.Command = program.option('-l, --list <items>', 'comma separated list', commaSeparatedList);

// requiredOption, same tests as option
const requiredOptionThis1: commander.Command = program.requiredOption('-a,--alpha');
const requiredOptionThis2: commander.Command = program.requiredOption('-p, --peppers', 'Add peppers')
const requiredOptionThis3: commander.Command = program.requiredOption('-s, --string [value]', 'default string', 'value')
const requiredOptionThis2: commander.Command = program.requiredOption('-p, --peppers', 'Add peppers');
const requiredOptionThis3: commander.Command = program.requiredOption('-s, --string [value]', 'default string', 'value');
const requiredOptionThis4: commander.Command = program.requiredOption('-b, --boolean', 'default boolean', false);

const requiredOptionThis5: commander.Command = program.requiredOption('-f, --float <number>', 'float argument', parseFloat)
const requiredOptionThis6: commander.Command = program.requiredOption('-f, --float <number>', 'float argument', parseFloat, 3.2)
const requiredOptionThis7: commander.Command = program.requiredOption('-i, --integer <number>', 'integer argument', myParseInt)
const requiredOptionThis8: commander.Command = program.requiredOption('-i, --integer <number>', 'integer argument', myParseInt, 5)
const requiredOptionThis9: commander.Command = program.requiredOption('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0)
const requiredOptionThis10: commander.Command = program.requiredOption('-c, --collect <value>', 'repeatable value', collect, [])
const requiredOptionThis5: commander.Command = program.requiredOption('-f, --float <number>', 'float argument', parseFloat);
const requiredOptionThis6: commander.Command = program.requiredOption('-f, --float <number>', 'float argument', parseFloat, 3.2);
const requiredOptionThis7: commander.Command = program.requiredOption('-i, --integer <number>', 'integer argument', myParseInt);
const requiredOptionThis8: commander.Command = program.requiredOption('-i, --integer <number>', 'integer argument', myParseInt, 5);
const requiredOptionThis9: commander.Command = program.requiredOption('-v, --verbose', 'verbosity that can be increased', increaseVerbosity, 0);
const requiredOptionThis10: commander.Command = program.requiredOption('-c, --collect <value>', 'repeatable value', collect, []);
const requiredOptionThis11: commander.Command = program.requiredOption('-l, --list <items>', 'comma separated list', commaSeparatedList);

// storeOptionsAsProperties
@@ -133,47 +143,51 @@ const parseThis1: commander.Command = program.parse();
const parseThis2: commander.Command = program.parse(process.argv);
const parseThis3: commander.Command = program.parse(['node', 'script.js'], { from: 'node' });
const parseThis4: commander.Command = program.parse(['node', 'script.js'], { from: 'electron' });
const parseThis5: commander.Command = program.parse(['--option'], { from: "user" });
const parseThis5: commander.Command = program.parse(['--option'], { from: 'user' });

// parseAsync, same tests as parse
const parseAsyncThis1: Promise<commander.Command> = program.parseAsync();
const parseAsyncThis2: Promise<commander.Command> = program.parseAsync(process.argv);
const parseAsyncThis3: Promise<commander.Command> = program.parseAsync(['node', 'script.js'], { from: 'node' });
const parseAsyncThis4: Promise<commander.Command> = program.parseAsync(['node', 'script.js'], { from: 'electron' });
const parseAsyncThis5: Promise<commander.Command> = program.parseAsync(['--option'], { from: "user" });
const parseAsyncThis5: Promise<commander.Command> = program.parseAsync(['--option'], { from: 'user' });

// parseOptions (and ParseOptionsResult)
const { operands, unknown } = program.parseOptions(['node', 'script.js', 'hello']);

// opts
const opts = program.opts();
const optsVal1 = opts.foo;
// eslint-disable-next-line dot-notation
const opstVale2 = opts['bar'];

// description
const descriptionThis: commander.Command = program.description("my description");
const descriptionThis: commander.Command = program.description('my description');
const descriptionValue: string = program.description();

// alias
const aliasThis: commander.Command = program.alias("my alias");
const aliasThis: commander.Command = program.alias('my alias');
const aliasValue: string = program.alias();

// aliases
const aliasesThis: commander.Command = program.aliases(['first-alias', 'second-alias']);
const aliasesValue: string[] = program.aliases();

// usage
const usageThis: commander.Command = program.usage("my usage");
const usageThis: commander.Command = program.usage('my usage');
const usageValue: string = program.usage();

// name
const nameThis: commander.Command = program.name("my-name");
const nameThis: commander.Command = program.name('my-name');
const nameValue: string = program.name();

// outputHelp
program.outputHelp();
program.outputHelp((str: string) => { return str });
program.outputHelp((str: string) => { return str; });

// help
program.help();
program.help((str: string) => { return str });

program.help((str: string) => { return str; });

// helpInformation
const helpInformnationValue: string = program.helpInformation();
@@ -184,18 +198,23 @@ const helpOptionThis2: commander.Command = program.helpOption('-h,--help', 'cust
const helpOptionThis3: commander.Command = program.helpOption(undefined, 'custom description');

// on
const onThis: commander.Command = program.on('--help', () => { })
const onThis: commander.Command = program.on('--help', () => {
// do nothing.
});

// createCommand

const createInstance1: commander.Command = program.createCommand();
const createInstance2: commander.Command = program.createCommand('name');

class MyCommand extends commander.Command {
createCommand(name?: string) {
createCommand(name?: string): MyCommand {
return new MyCommand(name);
}
myFunction() {}

myFunction(): void {
// do nothing.
}
}
const myProgram = new MyCommand();
myProgram.myFunction();
136 changes: 77 additions & 59 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ declare namespace commander {
message: string;
nestedError?: string;
}
type CommanderErrorConstructor = { new (exitCode: number, code: string, message: string): CommanderError };
type CommanderErrorConstructor = new (exitCode: number, code: string, message: string) => CommanderError;

interface Option {
flags: string;
@@ -21,7 +21,7 @@ declare namespace commander {
long: string;
description: string;
}
type OptionConstructor = { new (flags: string, description?: string): Option };
type OptionConstructor = new (flags: string, description?: string) => Option;

interface ParseOptions {
from: 'node' | 'electron' | 'user';
@@ -35,21 +35,21 @@ declare namespace commander {
commands: Command[];

/**
* Set the program version to `str`.
* Set the program version to `str`.
*
* This method auto-registers the "-V, --version" flag
* which will print the version number when passed.
*
*
* You can optionally supply the flags and description to override the defaults.
*/
version(str: string, flags?: string, description?: string): this;

/**
* Define a command, implemented using an action handler.
*
*
* @remarks
* The command description is supplied using `.description`, not as a parameter to `.command`.
*
*
* @example
* ```ts
* program
@@ -59,31 +59,31 @@ declare namespace commander {
* console.log('clone command called');
* });
* ```
*
*
* @param nameAndArgs - command name and arguments, args are `<required>` or `[optional]` and last may also be `variadic...`
* @param opts - configuration options
* @returns new command
*/
command(nameAndArgs: string, opts?: CommandOptions): ReturnType<this['createCommand']>;
/**
* Define a command, implemented in a separate executable file.
*
*
* @remarks
* The command description is supplied as the second parameter to `.command`.
*
*
* @example
* ```ts
* program
* .command('start <service>', 'start named service')
* .command('stop [service]', 'stop named serice, or all if no name supplied');
* ```
*
*
* @param nameAndArgs - command name and arguments, args are `<required>` or `[optional]` and last may also be `variadic...`
* @param description - description of executable command
* @param opts - configuration options
* @returns top level command for chaining more command definitions
* @returns `this` command for chaining
*/
command(nameAndArgs: string, description: string, opts?: commander.CommandOptions): this;
command(nameAndArgs: string, description: string, opts?: commander.ExecutableCommandOptions): this;

/**
* Factory routine to create a new unattached command.
@@ -92,20 +92,20 @@ declare namespace commander {
* create the command. You can override createCommand to customise subcommands.
*/
createCommand(name?: string): Command;

/**
* Add a prepared subcommand.
*
*
* See .command() for creating an attached subcommand which inherits settings from its parent.
*
* @returns parent command for chaining
*
* @returns `this` command for chaining
*/
addCommand(cmd: Command): this;
addCommand(cmd: Command, opts?: CommandOptions): this;

/**
* Define argument syntax for the top-level command.
* Define argument syntax for command.
*
* @returns Command for chaining
* @returns `this` command for chaining
*/
arguments(desc: string): this;

@@ -125,7 +125,7 @@ declare namespace commander {
* // output help here
* });
*
* @returns Command for chaining
* @returns `this` command for chaining
*/
action(fn: (...args: any[]) => void | Promise<void>): this;

@@ -169,7 +169,7 @@ declare namespace commander {
* // optional argument
* program.option('-c, --cheese [type]', 'add cheese [marble]');
*
* @returns Command for chaining
* @returns `this` command for chaining
*/
option(flags: string, description?: string, defaultValue?: string | boolean): this;
option(flags: string, description: string, regexp: RegExp, defaultValue?: string | boolean): this;
@@ -185,34 +185,33 @@ declare namespace commander {
requiredOption(flags: string, description: string, regexp: RegExp, defaultValue?: string | boolean): this;
requiredOption<T>(flags: string, description: string, fn: (value: string, previous: T) => T, defaultValue?: T): this;


/**
* Whether to store option values as properties on command object,
* or store separately (specify false). In both cases the option values can be accessed using .opts().
*
* @return Command for chaining
* @returns `this` command for chaining
*/
storeOptionsAsProperties(value?: boolean): this;

/**
* Whether to pass command to action handler,
* or just the options (specify false).
*
* @return Command for chaining
*
* @returns `this` command for chaining
*/
passCommandToAction(value?: boolean): this;

/**
* Allow unknown options on the command line.
*
* @param [arg] if `true` or omitted, no error will be thrown for unknown options.
* @returns Command for chaining
* @returns `this` command for chaining
*/
allowUnknownOption(arg?: boolean): this;

/**
* Parse `argv`, setting options and invoking commands when defined.
*
*
* The default expectation is that the arguments are from node and have the application as argv[0]
* and the script being run in argv[1], with user parameters after that.
*
@@ -221,16 +220,16 @@ declare namespace commander {
* program.parse(process.argv);
* program.parse(); // implicitly use process.argv and auto-detect node vs electron conventions
* program.parse(my-args, { from: 'user' }); // just user supplied arguments, nothing special about argv[0]
*
* @returns Command for chaining
*
* @returns `this` command for chaining
*/
parse(argv?: string[], options?: ParseOptions): this;

/**
* Parse `argv`, setting options and invoking commands when defined.
*
*
* Use parseAsync instead of parse if any of your action handlers are async. Returns a Promise.
*
*
* The default expectation is that the arguments are from node and have the application as argv[0]
* and the script being run in argv[1], with user parameters after that.
*
@@ -264,8 +263,8 @@ declare namespace commander {

/**
* Set the description.
*
* @returns Command for chaining
*
* @returns `this` command for chaining
*/
description(str: string, argsDescription?: {[argName: string]: string}): this;
/**
@@ -275,19 +274,34 @@ declare namespace commander {

/**
* Set an alias for the command.
*
* @returns Command for chaining
*
* You may call more than once to add multiple aliases. Only the first alias is shown in the auto-generated help.
*
* @returns `this` command for chaining
*/
alias(alias: string): this;
/**
* Get alias for the command.
*/
alias(): string;

/**
* Set aliases for the command.
*
* Only the first alias is shown in the auto-generated help.
*
* @returns `this` command for chaining
*/
aliases(aliases: string[]): this;
/**
* Get aliases for the command.
*/
aliases(): string[];

/**
* Set the command usage.
*
* @returns Command for chaining
*
* @returns `this` command for chaining
*/
usage(str: string): this;
/**
@@ -297,8 +311,8 @@ declare namespace commander {

/**
* Set the name of the command.
*
* @returns Command for chaining
*
* @returns `this` command for chaining
*/
name(str: string): this;
/**
@@ -318,21 +332,21 @@ declare namespace commander {
* Return command help documentation.
*/
helpInformation(): string;

/**
* You can pass in flags and a description to override the help
* flags and help description for your command.
*/
helpOption(flags?: string, description?: string): this;

/**
/**
* Output help information and exit.
*/
help(cb?: (str: string) => string): never;

/**
* Add a listener (callback) for when events occur. (Implemented using EventEmitter.)
*
*
* @example
* program
* .on('--help', () -> {
@@ -341,28 +355,32 @@ declare namespace commander {
*/
on(event: string | symbol, listener: (...args: any[]) => void): this;
}
type CommandConstructor = { new (name?: string): Command };

type CommandConstructor = new (name?: string) => Command;

interface CommandOptions {
noHelp?: boolean;
isDefault?: boolean;
executableFile?: string;
}
interface CommandOptions {
noHelp?: boolean; // old name for hidden
hidden?: boolean;
isDefault?: boolean;
}
interface ExecutableCommandOptions extends CommandOptions {
executableFile?: string;
}

interface ParseOptionsResult {
operands: string[];
unknown: string[];
}
interface ParseOptionsResult {
operands: string[];
unknown: string[];
}

interface CommanderStatic extends Command {
program: Command;
Command: CommandConstructor;
Option: OptionConstructor;
CommanderError:CommanderErrorConstructor;
}
interface CommanderStatic extends Command {
program: Command;
Command: CommandConstructor;
Option: OptionConstructor;
CommanderError: CommanderErrorConstructor;
}

}

// Declaring namespace AND global
// eslint-disable-next-line no-redeclare
declare const commander: commander.CommanderStatic;
export = commander;