Skip to content

Commit 8ac84ec

Browse files
authored
Positional options (#1427)
* 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
1 parent 1383870 commit 8ac84ec

8 files changed

+702
-3
lines changed

Readme.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md)
3535
- [Custom event listeners](#custom-event-listeners)
3636
- [Bits and pieces](#bits-and-pieces)
3737
- [.parse() and .parseAsync()](#parse-and-parseasync)
38+
- [Parsing Configuration](#parsing-configuration)
3839
- [Legacy options as properties](#legacy-options-as-properties)
3940
- [TypeScript](#typescript)
4041
- [createCommand()](#createcommand)
@@ -84,7 +85,7 @@ For example `-a -b -p 80` may be written as `-ab -p80` or even `-abp80`.
8485

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

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

8990
### Common option types, boolean and value
9091

@@ -684,6 +685,39 @@ program.parse(); // Implicit, and auto-detect electron
684685
program.parse(['-f', 'filename'], { from: 'user' });
685686
```
686687
688+
### Parsing Configuration
689+
690+
If the default parsing does not suit your needs, there are some behaviours to support other usage patterns.
691+
692+
By default program options are recognised before and after subcommands. To only look for program options before subcommands, use `.enablePositionalOptions()`. This lets you use
693+
an option for a different purpose in subcommands.
694+
695+
Example file: [positional-options.js](./examples/positional-options.js)
696+
697+
With positional options, the `-b` is a program option in the first line and a subcommand option in the second:
698+
699+
```sh
700+
program -b subcommand
701+
program subcommand -b
702+
```
703+
704+
By default options are recognised before and after command-arguments. To only process options that come
705+
before the command-arguments, use `.passThroughOptions()`. This lets you pass the arguments and following options through to another program
706+
without needing to use `--` to end the option processing.
707+
To use pass through options in a subcommand, the program needs to enable positional options.
708+
709+
Example file: [pass-through-options.js](./examples/pass-through-options.js)
710+
711+
With pass through options, the `--port=80` is a program option in the line and passed through as a command-argument in the second:
712+
713+
```sh
714+
program --port=80 arg
715+
program arg --port=80
716+
```
717+
718+
719+
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.
720+
687721
### Legacy options as properties
688722
689723
Before Commander 7, the option values were stored as properties on the command.

examples/pass-through-options.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env node
2+
3+
// const { Command } = require('commander'); // (normal include)
4+
const { Command } = require('../'); // include commander in git clone of commander repo
5+
const program = new Command();
6+
7+
program
8+
.arguments('<utility> [args...]')
9+
.passThroughOptions()
10+
.option('-d, --dry-run')
11+
.action((utility, args, options) => {
12+
const action = options.dryRun ? 'Would run' : 'Running';
13+
console.log(`${action}: ${utility} ${args.join(' ')}`);
14+
});
15+
16+
program.parse();
17+
18+
// Try the following:
19+
//
20+
// node pass-through-options.js git status
21+
// node pass-through-options.js git --version
22+
// node pass-through-options.js --dry-run git checkout -b new-branch
23+
// node pass-through-options.js git push --dry-run

examples/positional-options.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env node
2+
3+
// const { Command } = require('commander'); // (normal include)
4+
const { Command } = require('../'); // include commander in git clone of commander repo
5+
const program = new Command();
6+
7+
program
8+
.enablePositionalOptions()
9+
.option('-p, --progress');
10+
11+
program
12+
.command('upload <file>')
13+
.option('-p, --port <number>', 'port number', 80)
14+
.action((file, options) => {
15+
if (program.opts().progress) console.log('Starting upload...');
16+
console.log(`Uploading ${file} to port ${options.port}`);
17+
if (program.opts().progress) console.log('Finished upload');
18+
});
19+
20+
program.parse();
21+
22+
// Try the following:
23+
//
24+
// node positional-options.js upload test.js
25+
// node positional-options.js -p upload test.js
26+
// node positional-options.js upload -p 8080 test.js
27+
// node positional-options.js -p upload -p 8080 test.js

index.js

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,8 @@ class Command extends EventEmitter {
552552
this._combineFlagAndOptionalValue = true;
553553
this._description = '';
554554
this._argsDescription = undefined;
555+
this._enablePositionalOptions = false;
556+
this._passThroughOptions = false;
555557

556558
// see .configureOutput() for docs
557559
this._outputConfiguration = {
@@ -633,6 +635,7 @@ class Command extends EventEmitter {
633635
cmd._exitCallback = this._exitCallback;
634636
cmd._storeOptionsAsProperties = this._storeOptionsAsProperties;
635637
cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue;
638+
cmd._enablePositionalOptions = this._enablePositionalOptions;
636639

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

1139+
/**
1140+
* Enable positional options. Positional means global options are specified before subcommands which lets
1141+
* subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions.
1142+
* The default behaviour is non-positional and global options may appear anywhere on the command line.
1143+
*
1144+
* @param {Boolean} [positional=true]
1145+
*/
1146+
enablePositionalOptions(positional = true) {
1147+
this._enablePositionalOptions = !!positional;
1148+
return this;
1149+
};
1150+
1151+
/**
1152+
* Pass through options that come after command-arguments rather than treat them as command-options,
1153+
* so actual command-options come before command-arguments. Turning this on for a subcommand requires
1154+
* positional options to have been enabled on the program (parent commands).
1155+
* The default behaviour is non-positional and options may appear before or after command-arguments.
1156+
*
1157+
* @param {Boolean} [passThrough=true]
1158+
* for unknown options.
1159+
*/
1160+
passThroughOptions(passThrough = true) {
1161+
this._passThroughOptions = !!passThrough;
1162+
if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) {
1163+
throw new Error('passThroughOptions can not be used without turning on enablePositionOptions for parent command(s)');
1164+
}
1165+
return this;
1166+
};
1167+
11361168
/**
11371169
* Whether to store option values as properties on command object,
11381170
* or store separately (specify false). In both cases the option values can be accessed using .opts().
@@ -1609,11 +1641,38 @@ class Command extends EventEmitter {
16091641
}
16101642
}
16111643

1612-
// looks like an option but unknown, unknowns from here
1613-
if (arg.length > 1 && arg[0] === '-') {
1644+
// Not a recognised option by this command.
1645+
// Might be a command-argument, or subcommand option, or unknown option, or help command or option.
1646+
1647+
// An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands.
1648+
if (maybeOption(arg)) {
16141649
dest = unknown;
16151650
}
16161651

1652+
// If using positionalOptions, stop processing our options at subcommand.
1653+
if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) {
1654+
if (this._findCommand(arg)) {
1655+
operands.push(arg);
1656+
if (args.length > 0) unknown.push(...args);
1657+
break;
1658+
} else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) {
1659+
operands.push(arg);
1660+
if (args.length > 0) operands.push(...args);
1661+
break;
1662+
} else if (this._defaultCommandName) {
1663+
unknown.push(arg);
1664+
if (args.length > 0) unknown.push(...args);
1665+
break;
1666+
}
1667+
}
1668+
1669+
// If using passThroughOptions, stop processing options at first command-argument.
1670+
if (this._passThroughOptions) {
1671+
dest.push(arg);
1672+
if (args.length > 0) dest.push(...args);
1673+
break;
1674+
}
1675+
16171676
// add arg
16181677
dest.push(arg);
16191678
}

tests/command.chain.test.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,16 @@ describe('Command methods that should return this for chaining', () => {
141141
const result = program.configureOutput({ });
142142
expect(result).toBe(program);
143143
});
144+
145+
test('when call .passThroughOptions() then returns this', () => {
146+
const program = new Command();
147+
const result = program.passThroughOptions();
148+
expect(result).toBe(program);
149+
});
150+
151+
test('when call .enablePositionalOptions() then returns this', () => {
152+
const program = new Command();
153+
const result = program.enablePositionalOptions();
154+
expect(result).toBe(program);
155+
});
144156
});

0 commit comments

Comments
 (0)