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

Disallow excess arguments #2223

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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