Skip to content

Commit 58c28eb

Browse files
authored
Disallow excess arguments (#2223)
* Change default allowExcessArguments to false and fix tests * Add migration tips for allowExcessArguments change
1 parent 4fcb276 commit 58c28eb

11 files changed

+95
-16
lines changed

CHANGELOG.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,54 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
88
<!-- markdownlint-disable MD024 -->
99
<!-- markdownlint-disable MD004 -->
1010

11+
## [13.x] (date goes here)
12+
13+
### Changed
14+
15+
- *Breaking*: excess command-arguments cause an error by default
16+
17+
### Migration Tips
18+
19+
**Excess command-arguments**
20+
21+
It is now an error to pass more command-arguments than are expected.
22+
23+
Old code:
24+
25+
```js
26+
program.option('-p, --port <number>', 'port number');
27+
program.action((options) => {
28+
console.log(program.args);
29+
});
30+
```
31+
32+
```console
33+
$ node example.js a b c
34+
error: too many arguments. Expected 0 arguments but got 3.
35+
```
36+
37+
You can declare the expected arguments. The help will then be more accurate too. Note that declaring
38+
new arguments will change what is passed to the action handler.
39+
40+
```js
41+
program.option('-p, --port <number>', 'port number');
42+
program.argument('[args...]', 'remote command and arguments'); // expecting zero or more arguments
43+
program.action((args, options) => {
44+
console.log(args);
45+
});
46+
```
47+
48+
Or you could suppress the error without changing the rest of the code:
49+
50+
```js
51+
program.option('-p, --port', 'port number');
52+
program.allowExcessArguments();
53+
program.action((options) => {
54+
console.log(program.args);
55+
});
56+
```
57+
58+
1159
## [12.1.0] (2024-05-18)
1260

1361
### Added

lib/command.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Command extends EventEmitter {
2525
this.options = [];
2626
this.parent = null;
2727
this._allowUnknownOption = false;
28-
this._allowExcessArguments = true;
28+
this._allowExcessArguments = false;
2929
/** @type {Argument[]} */
3030
this.registeredArguments = [];
3131
this._args = this.registeredArguments; // deprecated old name

tests/args.literal.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ test('when arguments includes -- then stop processing options', () => {
99
const program = new commander.Command();
1010
program
1111
.option('-f, --foo', 'add some foo')
12-
.option('-b, --bar', 'add some bar');
12+
.option('-b, --bar', 'add some bar')
13+
.argument('[args...]');
1314
program.parse(['node', 'test', '--foo', '--', '--bar', 'baz']);
1415
// More than one assert, ported from legacy test
1516
const opts = program.opts();
@@ -22,7 +23,8 @@ test('when arguments include -- then more literals are passed-through as args',
2223
const program = new commander.Command();
2324
program
2425
.option('-f, --foo', 'add some foo')
25-
.option('-b, --bar', 'add some bar');
26+
.option('-b, --bar', 'add some bar')
27+
.argument('[args...]');
2628
program.parse(['node', 'test', '--', 'cmd', '--', '--arg']);
2729
expect(program.args).toEqual(['cmd', '--', '--arg']);
2830
});

tests/command.allowExcessArguments.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ describe.each([true, false])(
1414
}
1515
}
1616

17-
test('when specify excess program argument then no error by default', () => {
17+
test('when specify excess program argument then error by default', () => {
1818
const program = new commander.Command();
1919
configureCommand(program);
2020

2121
expect(() => {
2222
program.parse(['excess'], { from: 'user' });
23-
}).not.toThrow();
23+
}).toThrow();
2424
});
2525

2626
test('when specify excess program argument and allowExcessArguments(false) then error', () => {
@@ -53,14 +53,14 @@ describe.each([true, false])(
5353
}).not.toThrow();
5454
});
5555

56-
test('when specify excess command argument then no error (by default)', () => {
56+
test('when specify excess command argument then error (by default)', () => {
5757
const program = new commander.Command();
5858
const sub = program.command('sub');
5959
configureCommand(sub);
6060

6161
expect(() => {
6262
program.parse(['sub', 'excess'], { from: 'user' });
63-
}).not.toThrow();
63+
}).toThrow();
6464
});
6565

6666
test('when specify excess command argument and allowExcessArguments(false) then error', () => {

tests/command.allowUnknownOption.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe('allowUnknownOption', () => {
4646
program
4747
.exitOverride()
4848
.allowUnknownOption()
49+
.argument('[args...]') // unknown option will be passed as an argument
4950
.option('-p, --pepper', 'add pepper');
5051

5152
expect(() => {
@@ -58,6 +59,7 @@ describe('allowUnknownOption', () => {
5859
program
5960
.exitOverride()
6061
.allowUnknownOption(true)
62+
.argument('[args...]') // unknown option will be passed as an argument
6163
.option('-p, --pepper', 'add pepper');
6264

6365
expect(() => {

tests/command.copySettings.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ describe('copyInheritedSettings property tests', () => {
119119
const source = new commander.Command();
120120
const cmd = new commander.Command();
121121

122-
expect(cmd._allowExcessArguments).toBeTruthy();
123-
source.allowExcessArguments(false);
124-
cmd.copyInheritedSettings(source);
125122
expect(cmd._allowExcessArguments).toBeFalsy();
123+
source.allowExcessArguments();
124+
cmd.copyInheritedSettings(source);
125+
expect(cmd._allowExcessArguments).toBeTruthy();
126126
});
127127

128128
test('when copyInheritedSettings then copies enablePositionalOptions()', () => {

tests/command.hook.test.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,10 @@ describe('action hooks context', () => {
196196
program.argument('[arg]').hook('preAction', (thisCommand) => {
197197
expect(thisCommand.args).toEqual(['sub', 'value']);
198198
});
199-
program.command('sub').action(() => {});
199+
program
200+
.command('sub')
201+
.argument('<arg>')
202+
.action(() => {});
200203
program.parse(['sub', 'value'], { from: 'user' });
201204
});
202205

@@ -206,7 +209,10 @@ describe('action hooks context', () => {
206209
program.hook('preAction', (thisCommand, actionCommand) => {
207210
expect(actionCommand.args).toEqual(['value']);
208211
});
209-
program.command('sub').action(() => {});
212+
program
213+
.command('sub')
214+
.argument('<arg>')
215+
.action(() => {});
210216
program.parse(['sub', 'value'], { from: 'user' });
211217
});
212218
});

tests/command.parse.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const commander = require('../');
1010
describe('.parse() args from', () => {
1111
test('when no args then use process.argv and app/script/args', () => {
1212
const program = new commander.Command();
13+
program.argument('[args...]');
1314
const hold = process.argv;
1415
process.argv = 'node script.js user'.split(' ');
1516
program.parse();
@@ -19,6 +20,7 @@ describe('.parse() args from', () => {
1920

2021
test('when no args and electron properties and not default app then use process.argv and app/args', () => {
2122
const program = new commander.Command();
23+
program.argument('[args...]');
2224
const holdArgv = process.argv;
2325
process.versions.electron = '1.2.3';
2426
process.argv = 'node user'.split(' ');
@@ -30,18 +32,21 @@ describe('.parse() args from', () => {
3032

3133
test('when args then app/script/args', () => {
3234
const program = new commander.Command();
35+
program.argument('[args...]');
3336
program.parse('node script.js user'.split(' '));
3437
expect(program.args).toEqual(['user']);
3538
});
3639

3740
test('when args from "node" then app/script/args', () => {
3841
const program = new commander.Command();
42+
program.argument('[args...]');
3943
program.parse('node script.js user'.split(' '), { from: 'node' });
4044
expect(program.args).toEqual(['user']);
4145
});
4246

4347
test('when args from "electron" and not default app then app/args', () => {
4448
const program = new commander.Command();
49+
program.argument('[args...]');
4550
const hold = process.defaultApp;
4651
process.defaultApp = undefined;
4752
program.parse('customApp user'.split(' '), { from: 'electron' });
@@ -51,6 +56,7 @@ describe('.parse() args from', () => {
5156

5257
test('when args from "electron" and default app then app/script/args', () => {
5358
const program = new commander.Command();
59+
program.argument('[args...]');
5460
const hold = process.defaultApp;
5561
process.defaultApp = true;
5662
program.parse('electron script user'.split(' '), { from: 'electron' });
@@ -60,12 +66,14 @@ describe('.parse() args from', () => {
6066

6167
test('when args from "user" then args', () => {
6268
const program = new commander.Command();
69+
program.argument('[args...]');
6370
program.parse('user'.split(' '), { from: 'user' });
6471
expect(program.args).toEqual(['user']);
6572
});
6673

6774
test('when args from "silly" then throw', () => {
6875
const program = new commander.Command();
76+
program.argument('[args...]');
6977
expect(() => {
7078
program.parse(['node', 'script.js'], { from: 'silly' });
7179
}).toThrow();
@@ -75,6 +83,7 @@ describe('.parse() args from', () => {
7583
'when node execArgv includes %s then app/args',
7684
(flag) => {
7785
const program = new commander.Command();
86+
program.argument('[args...]');
7887
const holdExecArgv = process.execArgv;
7988
const holdArgv = process.argv;
8089
process.argv = ['node', 'user-arg'];
@@ -91,6 +100,7 @@ describe('.parse() args from', () => {
91100
describe('return type', () => {
92101
test('when call .parse then returns program', () => {
93102
const program = new commander.Command();
103+
program.argument('[args...]');
94104
program.action(() => {});
95105

96106
const result = program.parse(['node', 'test']);
@@ -99,6 +109,7 @@ describe('return type', () => {
99109

100110
test('when await .parseAsync then returns program', async () => {
101111
const program = new commander.Command();
112+
program.argument('[args...]');
102113
program.action(() => {});
103114

104115
const result = await program.parseAsync(['node', 'test']);
@@ -109,6 +120,7 @@ describe('return type', () => {
109120
// Easy mistake to make when writing unit tests
110121
test('when parse strings instead of array then throw', () => {
111122
const program = new commander.Command();
123+
program.argument('[args...]');
112124
expect(() => {
113125
program.parse('node', 'test');
114126
}).toThrow();
@@ -117,6 +129,7 @@ test('when parse strings instead of array then throw', () => {
117129
describe('parse parameter is treated as readonly, per TypeScript declaration', () => {
118130
test('when parse called then parameter does not change', () => {
119131
const program = new commander.Command();
132+
program.argument('[args...]');
120133
program.option('--debug');
121134
const original = ['node', '--debug', 'arg'];
122135
const param = original.slice();
@@ -126,6 +139,7 @@ describe('parse parameter is treated as readonly, per TypeScript declaration', (
126139

127140
test('when parse called and parsed args later changed then parameter does not change', () => {
128141
const program = new commander.Command();
142+
program.argument('[args...]');
129143
program.option('--debug');
130144
const original = ['node', '--debug', 'arg'];
131145
const param = original.slice();
@@ -137,6 +151,7 @@ describe('parse parameter is treated as readonly, per TypeScript declaration', (
137151

138152
test('when parse called and param later changed then parsed args do not change', () => {
139153
const program = new commander.Command();
154+
program.argument('[args...]');
140155
program.option('--debug');
141156
const param = ['node', '--debug', 'arg'];
142157
program.parse(param);
@@ -151,6 +166,7 @@ describe('parse parameter is treated as readonly, per TypeScript declaration', (
151166
describe('parseAsync parameter is treated as readonly, per TypeScript declaration', () => {
152167
test('when parse called then parameter does not change', async () => {
153168
const program = new commander.Command();
169+
program.argument('[args...]');
154170
program.option('--debug');
155171
const original = ['node', '--debug', 'arg'];
156172
const param = original.slice();
@@ -160,6 +176,7 @@ describe('parseAsync parameter is treated as readonly, per TypeScript declaratio
160176

161177
test('when parseAsync called and parsed args later changed then parameter does not change', async () => {
162178
const program = new commander.Command();
179+
program.argument('[args...]');
163180
program.option('--debug');
164181
const original = ['node', '--debug', 'arg'];
165182
const param = original.slice();
@@ -171,6 +188,7 @@ describe('parseAsync parameter is treated as readonly, per TypeScript declaratio
171188

172189
test('when parseAsync called and param later changed then parsed args do not change', async () => {
173190
const program = new commander.Command();
191+
program.argument('[args...]');
174192
program.option('--debug');
175193
const param = ['node', '--debug', 'arg'];
176194
await program.parseAsync(param);

tests/command.parseOptions.test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ describe('parseOptions', () => {
170170
describe('parse and program.args', () => {
171171
test('when program has known flag and operand then option removed and operand returned', () => {
172172
const program = new commander.Command();
173-
program.option('--global-flag');
173+
program.option('--global-flag').argument('[arg...]');
174174
program.parse('node test.js --global-flag arg'.split(' '));
175175
expect(program.args).toEqual(['arg']);
176176
});
@@ -180,7 +180,8 @@ describe('parse and program.args', () => {
180180
program
181181
.allowUnknownOption()
182182
.option('--global-flag')
183-
.option('--global-value <value>');
183+
.option('--global-value <value>')
184+
.argument('[arg...]');
184185
program.parse(
185186
'node test.js aaa --global-flag bbb --unknown ccc --global-value value'.split(
186187
' ',

tests/command.positionalOptions.test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ describe('program with allowUnknownOption', () => {
439439
test('when passThroughOptions and unknown option then arguments from unknown passed through', () => {
440440
const program = new commander.Command();
441441
program.passThroughOptions().allowUnknownOption().option('--debug');
442+
program.argument('[args...]');
442443

443444
program.parse(['--unknown', '--debug'], { from: 'user' });
444445
expect(program.args).toEqual(['--unknown', '--debug']);
@@ -447,6 +448,7 @@ describe('program with allowUnknownOption', () => {
447448
test('when positionalOptions and unknown option then known options then known option parsed', () => {
448449
const program = new commander.Command();
449450
program.enablePositionalOptions().allowUnknownOption().option('--debug');
451+
program.argument('[args...]');
450452

451453
program.parse(['--unknown', '--debug'], { from: 'user' });
452454
expect(program.opts().debug).toBe(true);

0 commit comments

Comments
 (0)