Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opt-in behaviour to avoid name pollution #1102

Merged
merged 17 commits into from Dec 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .eslintignore
@@ -0,0 +1,2 @@
# We have not configured eslint for TypeScript so avoid bogus error messages in the test file and typings file.
**/*.ts
3 changes: 2 additions & 1 deletion .eslintrc.json
Expand Up @@ -4,6 +4,7 @@
"rules": {
"one-var": "off",
"semi": ["error", "always"],
"space-before-function-paren": ["error", "never"]
"space-before-function-paren": ["error", "never"],
"no-else-return": ["error", { "allowElseIf": false }]
}
}
40 changes: 40 additions & 0 deletions Readme.md
Expand Up @@ -31,6 +31,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [.help(cb)](#helpcb)
- [Custom event listeners](#custom-event-listeners)
- [Bits and pieces](#bits-and-pieces)
- [Avoiding option name clashes](#avoiding-option-name-clashes)
- [TypeScript](#typescript)
- [Node options such as `--harmony`](#node-options-such-as---harmony)
- [Node debugging](#node-debugging)
Expand Down Expand Up @@ -70,6 +71,8 @@ Options are defined with the `.option()` method, also serving as documentation f

The options can be accessed as properties on the Command object. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc. Multiple short flags may be combined as a single arg, for example `-abc` is equivalent to `-a -b -c`.

See also optional new behaviour to [avoid name clashes](#avoiding-option-name-clashes).

### Common option types, boolean and value

The two most used option types are a boolean flag, and an option which takes a value (declared using angle brackets). Both are `undefined` unless specified on command line.
Expand Down Expand Up @@ -550,6 +553,43 @@ program.on('command:*', function () {

## Bits and pieces

### Avoiding option name clashes

The original and default behaviour is that the option values are stored
as properties on the program, and the action handler is passed a
command object with the options values stored as properties.
This is very convenient to code, but the downside is possible clashes with
existing properties of Command.

There are two new routines to change the behaviour, and the default behaviour may change in the future:

- `storeOptionsAsProperties`: whether to store option values as properties on command object, or store separately (specify false) and access using `.opts()`
- `passCommandToAction`: whether to pass command to action handler,
or just the options (specify false)

```js
// file: ./examples/storeOptionsAsProperties.action.js
program
.storeOptionsAsProperties(false)
.passCommandToAction(false);

program
.name('my-program-name')
.option('-n,--name <name>');

program
.command('show')
.option('-a,--action <action>')
.action((options) => {
console.log(options.action);
});

program.parse(process.argv);

const programOptions = program.opts();
console.log(programOptions.name);
```

### TypeScript

The Commander package includes its TypeScript Definition file, but also requires the node types which you need to install yourself. e.g.
Expand Down
40 changes: 40 additions & 0 deletions examples/storeOptionsAsProperties-action.js
@@ -0,0 +1,40 @@
#!/usr/bin/env node

// To avoid possible name clashes, you can change the default behaviour
// of storing the options values as properties on the command object.
// In addition, you can pass just the options to the action handler
// instead of a commmand object. This allows simpler code, and is more consistent
// with the previous behaviour so less code changes from old code.
//
// Example output:
//
// $ node storeOptionsAsProperties-action.js show
// undefined
// undefined
//
// $ node storeOptionsAsProperties-action.js --name foo show --action jump
// jump
// foo

const commander = require('../');
const program = new commander.Command();

program
.storeOptionsAsProperties(false) // <--- change behaviour
.passCommandToAction(false); // <--- change behaviour

program
.name('my-program-name')
.option('-n,--name <name>');

program
.command('show')
.option('-a,--action <action>')
.action((options) => { // <--- passed options, not Command
console.log(options.action); // <--- matches old code
});

program.parse(process.argv);

const programOptions = program.opts(); // <--- use opts to access option values
console.log(programOptions.name);
38 changes: 38 additions & 0 deletions examples/storeOptionsAsProperties-opts.js
@@ -0,0 +1,38 @@
#!/usr/bin/env node

// To avoid possible name clashes, you can change the default behaviour
// of storing the options values as properties on the command object.
// You access the option values using the .opts() function.
//
// Example output:
//
// $ node storeOptionsAsProperties-opts.js show
// undefined
// undefined
//
// $ node storeOptionsAsProperties-opts.js --name foo show --action jump
// jump
// foo

const commander = require('../');
const program = new commander.Command();

program
.storeOptionsAsProperties(false); // <--- change behaviour

program
.name('my-program-name')
.option('-n,--name <name>');

program
.command('show')
.option('-a,--action <action>')
.action((cmd) => {
const options = cmd.opts(); // <--- use opts to access option values
console.log(options.action);
});

program.parse(process.argv);

const programOptions = program.opts(); // <--- use opts to access option values
console.log(programOptions.name);
35 changes: 35 additions & 0 deletions examples/storeOptionsAsProperties-problem.js
@@ -0,0 +1,35 @@
#!/usr/bin/env node

// The original and default behaviour is that the option values are stored
// as properties on the program (Command). The action handler is passed a
// command object (Command) with the options values also stored as properties.
// This is very convenient to code, but the downside is possible clashes with
// existing properties of Command.
//
// Example output, note the issues in the first call:
//
// $ node storeOptionsAsProperties-problem.js show
// [Function]
// [Function]
//
// $ node storeOptionsAsProperties-problem.js --name foo show --action jump
// jump
// foo

const commander = require('../');
const program = new commander.Command();

program
.name('my-program-name')
.option('-n,--name <name>'); // Oops, clash with .name()

program
.command('show')
.option('-a,--action <action>') // Oops, clash with .action()
.action((cmd) => {
console.log(cmd.action);
});

program.parse(process.argv);

console.log(program.name);
113 changes: 95 additions & 18 deletions index.js
Expand Up @@ -126,6 +126,9 @@ function Command(name) {
this._allowUnknownOption = false;
this._args = [];
this._name = name || '';
this._optionValues = {};
this._storeOptionsAsProperties = true; // backwards compatible by default
this._passCommandToAction = true; // backwards compatible by default

this._helpFlags = '-h, --help';
this._helpDescription = 'output usage information';
Expand Down Expand Up @@ -183,6 +186,8 @@ Command.prototype.command = function(nameAndArgs, actionOptsOrExecDesc, execOpts
cmd._helpShortFlag = this._helpShortFlag;
cmd._helpLongFlag = this._helpLongFlag;
cmd._exitCallback = this._exitCallback;
cmd._storeOptionsAsProperties = this._storeOptionsAsProperties;
cmd._passCommandToAction = this._passCommandToAction;

cmd._executableFile = opts.executableFile; // Custom name for executable file
this.commands.push(cmd);
Expand Down Expand Up @@ -351,7 +356,11 @@ Command.prototype.action = function(fn) {
// The .action callback takes an extra parameter which is the command itself.
var expectedArgsCount = self._args.length;
var actionArgs = args.slice(0, expectedArgsCount);
actionArgs[expectedArgsCount] = self;
if (self._passCommandToAction) {
actionArgs[expectedArgsCount] = self;
} else {
actionArgs[expectedArgsCount] = self.opts();
}
// Add the extra arguments so available too.
if (args.length > expectedArgsCount) {
actionArgs.push(args.slice(expectedArgsCount));
Expand Down Expand Up @@ -405,12 +414,12 @@ Command.prototype._optionEx = function(config, flags, description, fn, defaultVa
if (option.negate || option.optional || option.required || typeof defaultValue === 'boolean') {
// when --no-foo we make sure default is true, unless a --foo option is already defined
if (option.negate) {
var opts = self.opts();
defaultValue = Object.prototype.hasOwnProperty.call(opts, name) ? opts[name] : true;
const positiveLongFlag = option.long.replace(/^--no-/, '--');
defaultValue = self.optionFor(positiveLongFlag) ? self._getOptionValue(name) : true;
}
// preassign only if we have a default
if (defaultValue !== undefined) {
self[name] = defaultValue;
self._setOptionValue(name, defaultValue);
option.defaultValue = defaultValue;
}
}
Expand All @@ -423,22 +432,22 @@ Command.prototype._optionEx = function(config, flags, description, fn, defaultVa
this.on('option:' + oname, function(val) {
// coercion
if (val !== null && fn) {
val = fn(val, self[name] === undefined ? defaultValue : self[name]);
val = fn(val, self._getOptionValue(name) === undefined ? defaultValue : self._getOptionValue(name));
}

// unassigned or boolean value
if (typeof self[name] === 'boolean' || typeof self[name] === 'undefined') {
if (typeof self._getOptionValue(name) === 'boolean' || typeof self._getOptionValue(name) === 'undefined') {
// if no value, negate false, and we have a default, then use it!
if (val == null) {
self[name] = option.negate
self._setOptionValue(name, option.negate
? false
: defaultValue || true;
: defaultValue || true);
} else {
self[name] = val;
self._setOptionValue(name, val);
}
} else if (val !== null) {
// reassign
self[name] = option.negate ? false : val;
self._setOptionValue(name, option.negate ? false : val);
}
});

Expand Down Expand Up @@ -531,6 +540,69 @@ Command.prototype.allowUnknownOption = function(arg) {
return 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().
*
* @param {boolean} value
* @return {Command} Command for chaining
* @api public
*/

Command.prototype.storeOptionsAsProperties = function(value) {
this._storeOptionsAsProperties = (value === undefined) || value;
if (this.options.length) {
// This is for programmer, not end user.
console.error('Commander usage error: call storeOptionsAsProperties before adding options');
}
return this;
};

/**
* Whether to pass command to action handler,
* or just the options (specify false).
*
* @param {boolean} value
* @return {Command} Command for chaining
* @api public
*/

Command.prototype.passCommandToAction = function(value) {
this._passCommandToAction = (value === undefined) || value;
return this;
};

/**
* Store option value
*
* @param {String} key
* @param {Object} value
* @api private
*/

Command.prototype._setOptionValue = function(key, value) {
if (this._storeOptionsAsProperties) {
this[key] = value;
} else {
this._optionValues[key] = value;
}
};

/**
* Retrieve option value
*
* @param {String} key
* @return {Object} value
* @api private
*/

Command.prototype._getOptionValue = function(key) {
if (this._storeOptionsAsProperties) {
return this[key];
}
return this._optionValues[key];
};

/**
* Parse `argv`, settings options and invoking commands when defined.
*
Expand Down Expand Up @@ -843,7 +915,7 @@ Command.prototype._checkForMissingMandatoryOptions = function() {
// Walk up hierarchy so can call from action handler after checking for displaying help.
for (var cmd = this; cmd; cmd = cmd.parent) {
cmd.options.forEach((anOption) => {
if (anOption.mandatory && (cmd[anOption.attributeName()] === undefined)) {
if (anOption.mandatory && (cmd._getOptionValue(anOption.attributeName()) === undefined)) {
cmd.missingMandatoryOptionValue(anOption);
}
});
Expand Down Expand Up @@ -936,14 +1008,19 @@ Command.prototype.parseOptions = function(argv) {
* @api public
*/
Command.prototype.opts = function() {
var result = {},
len = this.options.length;

for (var i = 0; i < len; i++) {
var key = this.options[i].attributeName();
result[key] = key === this._versionOptionName ? this._version : this[key];
if (this._storeOptionsAsProperties) {
// Preserve original behaviour so backwards compatible when still using properties
var result = {},
len = this.options.length;

for (var i = 0; i < len; i++) {
var key = this.options[i].attributeName();
result[key] = key === this._versionOptionName ? this._version : this[key];
}
return result;
}
return result;

return this._optionValues;
};

/**
Expand Down