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

Add .requiredOption() for mandatory options #1071

Merged
merged 10 commits into from Oct 8, 2019
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`);
});
});