Skip to content

Commit

Permalink
Disallow excess arguments (#2223)
Browse files Browse the repository at this point in the history
* Change default allowExcessArguments to false and fix tests
* Add migration tips for allowExcessArguments change
  • Loading branch information
shadowspawn committed Jul 8, 2024
1 parent 4fcb276 commit 58c28eb
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 16 deletions.
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,54 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
<!-- markdownlint-disable MD024 -->
<!-- markdownlint-disable MD004 -->

## [13.x] (date goes here)

### Changed

- *Breaking*: excess command-arguments cause an error by default

### Migration Tips

**Excess command-arguments**

It is now an error to pass more command-arguments than are expected.

Old code:

```js
program.option('-p, --port <number>', 'port number');
program.action((options) => {
console.log(program.args);
});
```

```console
$ node example.js a b c
error: too many arguments. Expected 0 arguments but got 3.
```

You can declare the expected arguments. The help will then be more accurate too. Note that declaring
new arguments will change what is passed to the action handler.

```js
program.option('-p, --port <number>', 'port number');
program.argument('[args...]', 'remote command and arguments'); // expecting zero or more arguments
program.action((args, options) => {
console.log(args);
});
```

Or you could suppress the error without changing the rest of the code:

```js
program.option('-p, --port', 'port number');
program.allowExcessArguments();
program.action((options) => {
console.log(program.args);
});
```


## [12.1.0] (2024-05-18)

### Added
Expand Down
2 changes: 1 addition & 1 deletion lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Command extends EventEmitter {
this.options = [];
this.parent = null;
this._allowUnknownOption = false;
this._allowExcessArguments = true;
this._allowExcessArguments = false;
/** @type {Argument[]} */
this.registeredArguments = [];
this._args = this.registeredArguments; // deprecated old name
Expand Down
6 changes: 4 additions & 2 deletions tests/args.literal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ test('when arguments includes -- then stop processing options', () => {
const program = new commander.Command();
program
.option('-f, --foo', 'add some foo')
.option('-b, --bar', 'add some bar');
.option('-b, --bar', 'add some bar')
.argument('[args...]');
program.parse(['node', 'test', '--foo', '--', '--bar', 'baz']);
// More than one assert, ported from legacy test
const opts = program.opts();
Expand All @@ -22,7 +23,8 @@ test('when arguments include -- then more literals are passed-through as args',
const program = new commander.Command();
program
.option('-f, --foo', 'add some foo')
.option('-b, --bar', 'add some bar');
.option('-b, --bar', 'add some bar')
.argument('[args...]');
program.parse(['node', 'test', '--', 'cmd', '--', '--arg']);
expect(program.args).toEqual(['cmd', '--', '--arg']);
});
8 changes: 4 additions & 4 deletions tests/command.allowExcessArguments.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ describe.each([true, false])(
}
}

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

expect(() => {
program.parse(['excess'], { from: 'user' });
}).not.toThrow();
}).toThrow();
});

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

test('when specify excess command argument then no error (by default)', () => {
test('when specify excess command argument then error (by default)', () => {
const program = new commander.Command();
const sub = program.command('sub');
configureCommand(sub);

expect(() => {
program.parse(['sub', 'excess'], { from: 'user' });
}).not.toThrow();
}).toThrow();
});

test('when specify excess command argument and allowExcessArguments(false) then error', () => {
Expand Down
2 changes: 2 additions & 0 deletions tests/command.allowUnknownOption.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ describe('allowUnknownOption', () => {
program
.exitOverride()
.allowUnknownOption()
.argument('[args...]') // unknown option will be passed as an argument
.option('-p, --pepper', 'add pepper');

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

expect(() => {
Expand Down
6 changes: 3 additions & 3 deletions tests/command.copySettings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ describe('copyInheritedSettings property tests', () => {
const source = new commander.Command();
const cmd = new commander.Command();

expect(cmd._allowExcessArguments).toBeTruthy();
source.allowExcessArguments(false);
cmd.copyInheritedSettings(source);
expect(cmd._allowExcessArguments).toBeFalsy();
source.allowExcessArguments();
cmd.copyInheritedSettings(source);
expect(cmd._allowExcessArguments).toBeTruthy();
});

test('when copyInheritedSettings then copies enablePositionalOptions()', () => {
Expand Down
10 changes: 8 additions & 2 deletions tests/command.hook.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,10 @@ describe('action hooks context', () => {
program.argument('[arg]').hook('preAction', (thisCommand) => {
expect(thisCommand.args).toEqual(['sub', 'value']);
});
program.command('sub').action(() => {});
program
.command('sub')
.argument('<arg>')
.action(() => {});
program.parse(['sub', 'value'], { from: 'user' });
});

Expand All @@ -206,7 +209,10 @@ describe('action hooks context', () => {
program.hook('preAction', (thisCommand, actionCommand) => {
expect(actionCommand.args).toEqual(['value']);
});
program.command('sub').action(() => {});
program
.command('sub')
.argument('<arg>')
.action(() => {});
program.parse(['sub', 'value'], { from: 'user' });
});
});
Expand Down
18 changes: 18 additions & 0 deletions tests/command.parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const commander = require('../');
describe('.parse() args from', () => {
test('when no args then use process.argv and app/script/args', () => {
const program = new commander.Command();
program.argument('[args...]');
const hold = process.argv;
process.argv = 'node script.js user'.split(' ');
program.parse();
Expand All @@ -19,6 +20,7 @@ describe('.parse() args from', () => {

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

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

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

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

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

test('when args from "user" then args', () => {
const program = new commander.Command();
program.argument('[args...]');
program.parse('user'.split(' '), { from: 'user' });
expect(program.args).toEqual(['user']);
});

test('when args from "silly" then throw', () => {
const program = new commander.Command();
program.argument('[args...]');
expect(() => {
program.parse(['node', 'script.js'], { from: 'silly' });
}).toThrow();
Expand All @@ -75,6 +83,7 @@ describe('.parse() args from', () => {
'when node execArgv includes %s then app/args',
(flag) => {
const program = new commander.Command();
program.argument('[args...]');
const holdExecArgv = process.execArgv;
const holdArgv = process.argv;
process.argv = ['node', 'user-arg'];
Expand All @@ -91,6 +100,7 @@ describe('.parse() args from', () => {
describe('return type', () => {
test('when call .parse then returns program', () => {
const program = new commander.Command();
program.argument('[args...]');
program.action(() => {});

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

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

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

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

test('when parse called and param later changed then parsed args do not change', () => {
const program = new commander.Command();
program.argument('[args...]');
program.option('--debug');
const param = ['node', '--debug', 'arg'];
program.parse(param);
Expand All @@ -151,6 +166,7 @@ describe('parse parameter is treated as readonly, per TypeScript declaration', (
describe('parseAsync parameter is treated as readonly, per TypeScript declaration', () => {
test('when parse called then parameter does not change', async () => {
const program = new commander.Command();
program.argument('[args...]');
program.option('--debug');
const original = ['node', '--debug', 'arg'];
const param = original.slice();
Expand All @@ -160,6 +176,7 @@ describe('parseAsync parameter is treated as readonly, per TypeScript declaratio

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

test('when parseAsync called and param later changed then parsed args do not change', async () => {
const program = new commander.Command();
program.argument('[args...]');
program.option('--debug');
const param = ['node', '--debug', 'arg'];
await program.parseAsync(param);
Expand Down
5 changes: 3 additions & 2 deletions tests/command.parseOptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ describe('parseOptions', () => {
describe('parse and program.args', () => {
test('when program has known flag and operand then option removed and operand returned', () => {
const program = new commander.Command();
program.option('--global-flag');
program.option('--global-flag').argument('[arg...]');
program.parse('node test.js --global-flag arg'.split(' '));
expect(program.args).toEqual(['arg']);
});
Expand All @@ -180,7 +180,8 @@ describe('parse and program.args', () => {
program
.allowUnknownOption()
.option('--global-flag')
.option('--global-value <value>');
.option('--global-value <value>')
.argument('[arg...]');
program.parse(
'node test.js aaa --global-flag bbb --unknown ccc --global-value value'.split(
' ',
Expand Down
2 changes: 2 additions & 0 deletions tests/command.positionalOptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ describe('program with allowUnknownOption', () => {
test('when passThroughOptions and unknown option then arguments from unknown passed through', () => {
const program = new commander.Command();
program.passThroughOptions().allowUnknownOption().option('--debug');
program.argument('[args...]');

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

program.parse(['--unknown', '--debug'], { from: 'user' });
expect(program.opts().debug).toBe(true);
Expand Down
4 changes: 2 additions & 2 deletions tests/command.unknownCommand.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ describe('unknownCommand', () => {
writeErrorSpy.mockRestore();
});

test('when unknown argument in simple program then no error', () => {
test('when unknown argument in simple program then error', () => {
const program = new commander.Command();
program.exitOverride();
expect(() => {
program.parse('node test.js unknown'.split(' '));
}).not.toThrow();
}).toThrow();
});

test('when unknown command but action handler taking arg then no error', () => {
Expand Down

0 comments on commit 58c28eb

Please sign in to comment.