Skip to content

Commit

Permalink
Add .requiredOption() for mandatory options (#1071)
Browse files Browse the repository at this point in the history
* Add declaration of requiredOption and mandatory property

* Add initial implementation for error detection for mandatory options

* Add unit tests for mandatory options

* Add detailed test for exitOverride intercept for missing mandatory option

* Add .requiredOption to README

* Reword mandatory description.

* Lookup option value using attributeName, not name

* Expand tests

- option name different than property name
- both of option/requiredOption for negated option
- add exitOverride to allow Jest to catch unexpected errors

* Add .requiredOption calls to TypeScript dummy program

* Tweak requiredOption example file
  • Loading branch information
shadowspawn committed Oct 8, 2019
1 parent 57e9d98 commit 7f9edba
Show file tree
Hide file tree
Showing 7 changed files with 416 additions and 49 deletions.
19 changes: 19 additions & 0 deletions Readme.md
Expand Up @@ -15,6 +15,7 @@ The complete solution for [node.js](http://nodejs.org) command-line interfaces,
- [Default option value](#default-option-value)
- [Other option types, negatable boolean and flag|value](#other-option-types-negatable-boolean-and-flagvalue)
- [Custom option processing](#custom-option-processing)
- [Required option](#required-option)
- [Version option](#version-option)
- [Commands](#commands)
- [Specify the argument syntax](#specify-the-argument-syntax)
Expand Down Expand Up @@ -242,6 +243,24 @@ $ custom --list x,y,z
[ 'x', 'y', 'z' ]
```
### Required option
You may specify a required (mandatory) option using `.requiredOption`. The option must be specified on the command line, or by having a default value. The method is otherwise the same as `.option` in format, taking flags and description, and optional default value or custom processing.
```js
const program = require('commander');

program
.requiredOption('-c, --cheese <type>', 'pizza must have cheese');

program.parse(process.argv);
```
```
$ pizza
error: required option '-c, --cheese <type>' not specified
```
### Version option
The optional `version` method adds handling for displaying the command version. The default option flags are `-V` and `--version`, and when present the command prints the version number and exits.
Expand Down
19 changes: 19 additions & 0 deletions examples/options-required.js
@@ -0,0 +1,19 @@
#!/usr/bin/env node

// This is used as an example in the README for:
// Required option
// You may specify a required (mandatory) option using `.requiredOption`.
// The option must be specified on the command line, or by having a default value.
//
// Example output pretending command called pizza (or try directly with `node options-required.js`)
//
// $ pizza
// error: required option '-c, --cheese <type>' not specified

const commander = require('..'); // For running direct from git clone of commander repo
const program = new commander.Command();

program
.requiredOption('-c, --cheese <type>', 'pizza must have cheese');

program.parse(process.argv);
160 changes: 113 additions & 47 deletions index.js
Expand Up @@ -43,8 +43,9 @@ exports.Option = Option;

function Option(flags, description) {
this.flags = flags;
this.required = flags.indexOf('<') >= 0;
this.optional = flags.indexOf('[') >= 0;
this.required = flags.indexOf('<') >= 0; // A value must be supplied when the option is specified.
this.optional = flags.indexOf('[') >= 0; // A value is optional when the option is specified.
this.mandatory = false; // The option must have a value after parsing, which usually means it must be specified on command line.
this.negate = flags.indexOf('-no-') !== -1;
flags = flags.split(/[ ,|]+/);
if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
Expand Down Expand Up @@ -365,62 +366,23 @@ Command.prototype.action = function(fn) {
};

/**
* Define option with `flags`, `description` and optional
* coercion `fn`.
*
* The `flags` string should contain both the short and long flags,
* separated by comma, a pipe or space. The following are all valid
* all will output this way when `--help` is used.
*
* "-p, --pepper"
* "-p|--pepper"
* "-p --pepper"
*
* Examples:
*
* // simple boolean defaulting to undefined
* program.option('-p, --pepper', 'add pepper');
*
* program.pepper
* // => undefined
*
* --pepper
* program.pepper
* // => true
*
* // simple boolean defaulting to true (unless non-negated option is also defined)
* program.option('-C, --no-cheese', 'remove cheese');
*
* program.cheese
* // => true
*
* --no-cheese
* program.cheese
* // => false
*
* // required argument
* program.option('-C, --chdir <path>', 'change the working directory');
*
* --chdir /tmp
* program.chdir
* // => "/tmp"
*
* // optional argument
* program.option('-c, --cheese [type]', 'add cheese [marble]');
* Internal implementation shared by .option() and .requiredOption()
*
* @param {Object} config
* @param {String} flags
* @param {String} description
* @param {Function|*} [fn] or default
* @param {Function|*} [fn] - custom option processing function or default vaue
* @param {*} [defaultValue]
* @return {Command} for chaining
* @api public
* @api private
*/

Command.prototype.option = function(flags, description, fn, defaultValue) {
Command.prototype._optionEx = function(config, flags, description, fn, defaultValue) {
var self = this,
option = new Option(flags, description),
oname = option.name(),
name = option.attributeName();
option.mandatory = !!config.mandatory;

// default as 3rd arg
if (typeof fn !== 'function') {
Expand Down Expand Up @@ -482,6 +444,80 @@ Command.prototype.option = function(flags, description, fn, defaultValue) {
return this;
};

/**
* Define option with `flags`, `description` and optional
* coercion `fn`.
*
* The `flags` string should contain both the short and long flags,
* separated by comma, a pipe or space. The following are all valid
* all will output this way when `--help` is used.
*
* "-p, --pepper"
* "-p|--pepper"
* "-p --pepper"
*
* Examples:
*
* // simple boolean defaulting to undefined
* program.option('-p, --pepper', 'add pepper');
*
* program.pepper
* // => undefined
*
* --pepper
* program.pepper
* // => true
*
* // simple boolean defaulting to true (unless non-negated option is also defined)
* program.option('-C, --no-cheese', 'remove cheese');
*
* program.cheese
* // => true
*
* --no-cheese
* program.cheese
* // => false
*
* // required argument
* program.option('-C, --chdir <path>', 'change the working directory');
*
* --chdir /tmp
* program.chdir
* // => "/tmp"
*
* // optional argument
* program.option('-c, --cheese [type]', 'add cheese [marble]');
*
* @param {String} flags
* @param {String} description
* @param {Function|*} [fn] - custom option processing function or default vaue
* @param {*} [defaultValue]
* @return {Command} for chaining
* @api public
*/

Command.prototype.option = function(flags, description, fn, defaultValue) {
return this._optionEx({}, flags, description, fn, defaultValue);
};

/*
* Add a required option which must have a value after parsing. This usually means
* the option must be specified on the command line. (Otherwise the same as .option().)
*
* The `flags` string should contain both the short and long flags, separated by comma, a pipe or space.
*
* @param {String} flags
* @param {String} description
* @param {Function|*} [fn] - custom option processing function or default vaue
* @param {*} [defaultValue]
* @return {Command} for chaining
* @api public
*/

Command.prototype.requiredOption = function(flags, description, fn, defaultValue) {
return this._optionEx({ mandatory: true }, flags, description, fn, defaultValue);
};

/**
* Allow unknown options on the command line.
*
Expand Down Expand Up @@ -789,6 +825,21 @@ Command.prototype.optionFor = function(arg) {
}
};

/**
* Display an error message if a mandatory option does not have a value.
*
* @api private
*/

Command.prototype._checkForMissingMandatoryOptions = function() {
const self = this;
this.options.forEach((anOption) => {
if (anOption.mandatory && (self[anOption.attributeName()] === undefined)) {
self.missingMandatoryOptionValue(anOption);
}
});
};

/**
* Parse options from `argv` returning `argv`
* void of these options.
Expand Down Expand Up @@ -865,6 +916,8 @@ Command.prototype.parseOptions = function(argv) {
args.push(arg);
}

this._checkForMissingMandatoryOptions();

return { args: args, unknown: unknownOptions };
};

Expand Down Expand Up @@ -917,6 +970,19 @@ Command.prototype.optionMissingArgument = function(option, flag) {
this._exit(1, 'commander.optionMissingArgument', message);
};

/**
* `Option` does not have a value, and is a mandatory option.
*
* @param {String} option
* @api private
*/

Command.prototype.missingMandatoryOptionValue = function(option) {
const message = `error: required option '${option.flags}' not specified`;
console.error(message);
this._exit(1, 'commander.missingMandatoryOptionValue', message);
};

/**
* Unknown option `flag`.
*
Expand Down
17 changes: 17 additions & 0 deletions tests/command.exitOverride.test.js
Expand Up @@ -210,4 +210,21 @@ describe('.exitOverride and error details', () => {

program.parse(['node', pm, 'does-not-exist']);
});

test('when mandatory program option missing then throw CommanderError', () => {
const optionFlags = '-p, --pepper <type>';
const program = new commander.Command();
program
.exitOverride()
.requiredOption(optionFlags, 'add pepper');

let caughtErr;
try {
program.parse(['node', 'test']);
} catch (err) {
caughtErr = err;
}

expectCommanderError(caughtErr, 1, 'commander.missingMandatoryOptionValue', `error: required option '${optionFlags}' not specified`);
});
});

0 comments on commit 7f9edba

Please sign in to comment.