Skip to content

Commit

Permalink
Positional options (#1427)
Browse files Browse the repository at this point in the history
* First cut at optionsBeforeArguments

* Different to mix global options and subcommands, and options and arguments.

* Different to mix global options and subcommands, and options and arguments.

* Add _parseOptionsFollowingArguments

* Use allow wording

* Another try at naming

* Exclude options from special processing, which fixes help

* Add help checks for new option configuration

* Rename after discovering needed for any positional options

* Rework logic to hopefully cope with default commands.

* Expand basic tests. Positional options are tricky!

* Add first default command tests

* Fill out more tests

* Add setters, and throw when passThrough without enabling positional

* Rename test file

* Add TypeScript

* Add tests. Fix help handling by making explicit.

* Reorder tests

* Use usual indentation

* Make _enablePositionalOptions inherited to simpify nested commands

* Add examples

* Add tests for some less common setups

* Test the boring true/false parameters

* Fix typo

* Add new section to README with parsing configuration.

* Tweak wording in README
  • Loading branch information
shadowspawn committed Jan 4, 2021
1 parent 1383870 commit 8ac84ec
Show file tree
Hide file tree
Showing 8 changed files with 702 additions and 3 deletions.
36 changes: 35 additions & 1 deletion Readme.md
Expand Up @@ -35,6 +35,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
- [Custom event listeners](#custom-event-listeners)
- [Bits and pieces](#bits-and-pieces)
- [.parse() and .parseAsync()](#parse-and-parseasync)
- [Parsing Configuration](#parsing-configuration)
- [Legacy options as properties](#legacy-options-as-properties)
- [TypeScript](#typescript)
- [createCommand()](#createcommand)
Expand Down Expand Up @@ -84,7 +85,7 @@ For example `-a -b -p 80` may be written as `-ab -p80` or even `-abp80`.

You can use `--` to indicate the end of the options, and any remaining arguments will be used without being interpreted.

Options on the command line are not positional, and can be specified before or after other arguments.
By default options on the command line are not positional, and can be specified before or after other arguments.

### Common option types, boolean and value

Expand Down Expand Up @@ -684,6 +685,39 @@ program.parse(); // Implicit, and auto-detect electron
program.parse(['-f', 'filename'], { from: 'user' });
```
### Parsing Configuration
If the default parsing does not suit your needs, there are some behaviours to support other usage patterns.
By default program options are recognised before and after subcommands. To only look for program options before subcommands, use `.enablePositionalOptions()`. This lets you use
an option for a different purpose in subcommands.
Example file: [positional-options.js](./examples/positional-options.js)
With positional options, the `-b` is a program option in the first line and a subcommand option in the second:
```sh
program -b subcommand
program subcommand -b
```
By default options are recognised before and after command-arguments. To only process options that come
before the command-arguments, use `.passThroughOptions()`. This lets you pass the arguments and following options through to another program
without needing to use `--` to end the option processing.
To use pass through options in a subcommand, the program needs to enable positional options.
Example file: [pass-through-options.js](./examples/pass-through-options.js)
With pass through options, the `--port=80` is a program option in the line and passed through as a command-argument in the second:
```sh
program --port=80 arg
program arg --port=80
```
By default the option processing shows an error for an unknown option. To have an unknown option treated as an ordinary command-argument and continue looking for options, use `.allowUnknownOption()`. This lets you mix known and unknown options.
### Legacy options as properties
Before Commander 7, the option values were stored as properties on the command.
Expand Down
23 changes: 23 additions & 0 deletions examples/pass-through-options.js
@@ -0,0 +1,23 @@
#!/usr/bin/env node

// const { Command } = require('commander'); // (normal include)
const { Command } = require('../'); // include commander in git clone of commander repo
const program = new Command();

program
.arguments('<utility> [args...]')
.passThroughOptions()
.option('-d, --dry-run')
.action((utility, args, options) => {
const action = options.dryRun ? 'Would run' : 'Running';
console.log(`${action}: ${utility} ${args.join(' ')}`);
});

program.parse();

// Try the following:
//
// node pass-through-options.js git status
// node pass-through-options.js git --version
// node pass-through-options.js --dry-run git checkout -b new-branch
// node pass-through-options.js git push --dry-run
27 changes: 27 additions & 0 deletions examples/positional-options.js
@@ -0,0 +1,27 @@
#!/usr/bin/env node

// const { Command } = require('commander'); // (normal include)
const { Command } = require('../'); // include commander in git clone of commander repo
const program = new Command();

program
.enablePositionalOptions()
.option('-p, --progress');

program
.command('upload <file>')
.option('-p, --port <number>', 'port number', 80)
.action((file, options) => {
if (program.opts().progress) console.log('Starting upload...');
console.log(`Uploading ${file} to port ${options.port}`);
if (program.opts().progress) console.log('Finished upload');
});

program.parse();

// Try the following:
//
// node positional-options.js upload test.js
// node positional-options.js -p upload test.js
// node positional-options.js upload -p 8080 test.js
// node positional-options.js -p upload -p 8080 test.js
63 changes: 61 additions & 2 deletions index.js
Expand Up @@ -552,6 +552,8 @@ class Command extends EventEmitter {
this._combineFlagAndOptionalValue = true;
this._description = '';
this._argsDescription = undefined;
this._enablePositionalOptions = false;
this._passThroughOptions = false;

// see .configureOutput() for docs
this._outputConfiguration = {
Expand Down Expand Up @@ -633,6 +635,7 @@ class Command extends EventEmitter {
cmd._exitCallback = this._exitCallback;
cmd._storeOptionsAsProperties = this._storeOptionsAsProperties;
cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue;
cmd._enablePositionalOptions = this._enablePositionalOptions;

cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor
this.commands.push(cmd);
Expand Down Expand Up @@ -1133,6 +1136,35 @@ class Command extends EventEmitter {
return this;
};

/**
* Enable positional options. Positional means global options are specified before subcommands which lets
* subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions.
* The default behaviour is non-positional and global options may appear anywhere on the command line.
*
* @param {Boolean} [positional=true]
*/
enablePositionalOptions(positional = true) {
this._enablePositionalOptions = !!positional;
return this;
};

/**
* Pass through options that come after command-arguments rather than treat them as command-options,
* so actual command-options come before command-arguments. Turning this on for a subcommand requires
* positional options to have been enabled on the program (parent commands).
* The default behaviour is non-positional and options may appear before or after command-arguments.
*
* @param {Boolean} [passThrough=true]
* for unknown options.
*/
passThroughOptions(passThrough = true) {
this._passThroughOptions = !!passThrough;
if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) {
throw new Error('passThroughOptions can not be used without turning on enablePositionOptions for parent command(s)');
}
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().
Expand Down Expand Up @@ -1609,11 +1641,38 @@ class Command extends EventEmitter {
}
}

// looks like an option but unknown, unknowns from here
if (arg.length > 1 && arg[0] === '-') {
// Not a recognised option by this command.
// Might be a command-argument, or subcommand option, or unknown option, or help command or option.

// An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands.
if (maybeOption(arg)) {
dest = unknown;
}

// If using positionalOptions, stop processing our options at subcommand.
if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) {
if (this._findCommand(arg)) {
operands.push(arg);
if (args.length > 0) unknown.push(...args);
break;
} else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) {
operands.push(arg);
if (args.length > 0) operands.push(...args);
break;
} else if (this._defaultCommandName) {
unknown.push(arg);
if (args.length > 0) unknown.push(...args);
break;
}
}

// If using passThroughOptions, stop processing options at first command-argument.
if (this._passThroughOptions) {
dest.push(arg);
if (args.length > 0) dest.push(...args);
break;
}

// add arg
dest.push(arg);
}
Expand Down
12 changes: 12 additions & 0 deletions tests/command.chain.test.js
Expand Up @@ -141,4 +141,16 @@ describe('Command methods that should return this for chaining', () => {
const result = program.configureOutput({ });
expect(result).toBe(program);
});

test('when call .passThroughOptions() then returns this', () => {
const program = new Command();
const result = program.passThroughOptions();
expect(result).toBe(program);
});

test('when call .enablePositionalOptions() then returns this', () => {
const program = new Command();
const result = program.enablePositionalOptions();
expect(result).toBe(program);
});
});

0 comments on commit 8ac84ec

Please sign in to comment.