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

Positional options #1427

Merged
merged 26 commits into from Jan 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0f74c8b
First cut at optionsBeforeArguments
shadowspawn Dec 27, 2020
3de2bde
Different to mix global options and subcommands, and options and argu…
shadowspawn Dec 27, 2020
bc1f8f7
Different to mix global options and subcommands, and options and argu…
shadowspawn Dec 27, 2020
c93da58
Add _parseOptionsFollowingArguments
shadowspawn Dec 27, 2020
69458b4
Use allow wording
shadowspawn Dec 27, 2020
304ad64
Another try at naming
shadowspawn Dec 28, 2020
ee98c44
Exclude options from special processing, which fixes help
shadowspawn Dec 28, 2020
e155f3a
Add help checks for new option configuration
shadowspawn Dec 28, 2020
fe29f6e
Rename after discovering needed for any positional options
shadowspawn Dec 29, 2020
d781d77
Rework logic to hopefully cope with default commands.
shadowspawn Dec 29, 2020
eef4d52
Expand basic tests. Positional options are tricky!
shadowspawn Dec 29, 2020
f20aff0
Add first default command tests
shadowspawn Dec 30, 2020
91468d3
Fill out more tests
shadowspawn Dec 31, 2020
3b6cd3f
Add setters, and throw when passThrough without enabling positional
shadowspawn Jan 1, 2021
bb95593
Rename test file
shadowspawn Jan 1, 2021
9c5b77a
Add TypeScript
shadowspawn Jan 1, 2021
6a4524b
Add tests. Fix help handling by making explicit.
shadowspawn Jan 2, 2021
4d26a15
Reorder tests
shadowspawn Jan 2, 2021
4136ce8
Use usual indentation
shadowspawn Jan 2, 2021
17472b9
Make _enablePositionalOptions inherited to simpify nested commands
shadowspawn Jan 2, 2021
2c9373f
Add examples
shadowspawn Jan 2, 2021
ca47d95
Add tests for some less common setups
shadowspawn Jan 2, 2021
a021878
Test the boring true/false parameters
shadowspawn Jan 2, 2021
7ab4d6d
Fix typo
shadowspawn Jan 2, 2021
71d92e1
Add new section to README with parsing configuration.
shadowspawn Jan 3, 2021
6f7ffcb
Tweak wording in README
shadowspawn Jan 3, 2021
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
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);
});
});