Skip to content

Commit

Permalink
feat: Implicit boolean option (#109)
Browse files Browse the repository at this point in the history
Implicit boolean option support

A option which doesn't take a value is called as an implicit boolean
option.

A typical implicit boolean option is defined like below:

```
program
  .command('command', 'Command')
  .option('-f, --flag', 'Flag')
  ...
```

The default value of any implicit boolean option is always `true` even
though a value other than `false` is explicitly specified as its
default value.

There is no meaning to specify arguments other than `synopsis` and
`description`.  Because:

* `validator`: The option has no value to be validate
* `defaultValue`: The default value is always `false`
* `required`: Setting it `true` always makes the option value `true`
  • Loading branch information
masnagam authored and mattallty committed Oct 13, 2018
1 parent d712dd8 commit bf25c33
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 7 deletions.
4 changes: 2 additions & 2 deletions lib/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ class Autocomplete {
}

_complete(data, done) {
const args = parseArgs(data.args.slice(1));
const currCommand = this._findCommand(data.args.slice(3).join(' '));
const args = parseArgs(data.args.slice(1), currCommand ? currCommand.parseArgsOpts : {});
const cmd = args._.join(' ');
const currCommand = this._findCommand(cmd);



Expand Down
4 changes: 4 additions & 0 deletions lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Command extends GetterSetter {
this._lastAddedArgOrOpt = null;
this._visible = true;
this._setupLoggerMethods();
this.parseArgsOpts = { boolean: [] };
}


Expand Down Expand Up @@ -376,6 +377,9 @@ class Command extends GetterSetter {
const opt = new Option(synopsis, description, validator, defaultValue, required, this._program);
this._lastAddedArgOrOpt = opt;
this._options.push(opt);
if (opt.isImplicitBoolean()) {
this.parseArgsOpts.boolean.push(opt.getLongOrShortName());
}
return this;
}

Expand Down
6 changes: 6 additions & 0 deletions lib/option.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class Option extends GetterSetter {
this._long = analysis.long;
this._booleanFlag = analysis.booleanFlag;
this._name = this.getCleanNameFromNotation(this._longCleanName || this._shortCleanName);
if (this.isImplicitBoolean())
this._default = false;
}

hasDefault() {
Expand All @@ -46,6 +48,10 @@ class Option extends GetterSetter {
return this._validator ? this._validator.getChoices() : [];
}

isImplicitBoolean() {
return this._valueType === undefined;
}

isRequired() {
return this._required;
}
Expand Down
5 changes: 4 additions & 1 deletion lib/program.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@ class Program extends GetterSetter {
*/
parse(argv) {
const argvSlice = argv.slice(2);
const args = parseArgs(argv);
let cmd = this._commands.filter(c => (c.name() === argvSlice[0] || c.getAlias() === argvSlice[0]))[0];
if (!cmd)
cmd = this._getDefaultCommand(false);
const args = parseArgs(argv, cmd ? cmd.parseArgsOpts : {});
let options = Object.assign({}, args);
delete options._;

Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"lodash.merge": "^4.6.0",
"micromist": "^1.0.1",
"micromist": "1.1.0",
"prettyjson": "^1.2.1",
"tabtab": "^2.2.2",
"winston": "^2.3.1"
Expand Down
128 changes: 128 additions & 0 deletions tests/issues/issue-107-implicit-boolean-option.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"use strict";

/* global Program, logger, should, makeArgv, sinon */

describe('Issue #107 - Implicit boolean option', () => {
context('having the shorthand and the longhand', () => {
beforeEach(() => {
this.program = new Program();
this.action = sinon.spy();
this.program
.logger(logger)
.version('1.0.0')
.command('cmd', 'Command')
.option('-b, --bool', 'Implicit boolean')
.argument('[a]', 'A', this.program.INT)
.action(this.action);
this.fatalError = sinon.stub(this.program, "fatalError");
});

afterEach(() => {
this.fatalError.restore();
this.program.reset();
});

it(`should call the action with {a: 1} and {bool: true}`, () => {
this.program.parse(makeArgv(['cmd', '-b', '1']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({a: 1}, {bool: true}, logger));
});

it(`should call the action with {} and {bool: true}`, () => {
this.program.parse(makeArgv(['cmd', '-b']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({}, {bool: true}, logger));
});

it(`should call the action with {a: 1} and {bool: false}`, () => {
this.program.parse(makeArgv(['cmd', '1']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({a: 1}, {bool: false}, logger));
});
});

context('only having the longhand', () => {
beforeEach(() => {
this.program = new Program();
this.action = sinon.spy();
this.program
.logger(logger)
.version('1.0.0')
.command('cmd', 'Command')
.option('--bool', 'Implicit boolean')
.argument('[a]', 'A', this.program.INT)
.action(this.action);
this.fatalError = sinon.stub(this.program, "fatalError");
});

afterEach(() => {
this.fatalError.restore();
this.program.reset();
});

it(`should call the action with {a: 1} and {bool: true}`, () => {
this.program.parse(makeArgv(['cmd', '--bool', '1']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({a: 1}, {bool: true}, logger));
});

it(`should call the action with {} and {bool: true}`, () => {
this.program.parse(makeArgv(['cmd', '--bool']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({}, {bool: true}, logger));
});

it(`should call the action with {a: 1} and {bool: false}`, () => {
this.program.parse(makeArgv(['cmd', '1']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({a: 1}, {bool: false}, logger));
});
});

context('only having the shorthand', () => {
beforeEach(() => {
this.program = new Program();
this.action = sinon.spy();
this.program
.logger(logger)
.version('1.0.0')
.command('cmd', 'Command')
.option('-b', 'Implicit boolean')
.argument('[a]', 'A', this.program.INT)
.action(this.action);
this.fatalError = sinon.stub(this.program, "fatalError");
});

afterEach(() => {
this.fatalError.restore();
this.program.reset();
});

it(`should call the action with {a: 1} and {b: true}`, () => {
this.program.parse(makeArgv(['cmd', '-b', '1']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({a: 1}, {b: true}, logger));
});

it(`should call the action with {} and {b: true}`, () => {
this.program.parse(makeArgv(['cmd', '-b']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({}, {b: true}, logger));
});

it(`should call the action with {a: 1} and {b: false}`, () => {
this.program.parse(makeArgv(['cmd', '1']));
should(this.fatalError.callCount).eql(0);
should(this.action.callCount).eql(1);
should(this.action.calledWith({a: 1}, {b: false}, logger));
});
});
});

0 comments on commit bf25c33

Please sign in to comment.