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

WIP: Reject excess arguments by default #1399

Closed
Closed
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
38 changes: 33 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ class Command extends EventEmitter {
this.options = [];
this.parent = null;
this._allowUnknownOption = false;
this._allowExcessArguments = false;
this._args = [];
this.rawArgs = null;
this._scriptPath = null;
Expand Down Expand Up @@ -882,18 +883,20 @@ class Command extends EventEmitter {

action(fn) {
const listener = (args) => {
// The .action callback takes an extra parameter which is the command or options.
const expectedArgsCount = this._args.length;
const actionArgs = args.slice(0, expectedArgsCount);
if (args.length > expectedArgsCount && this._actionHandler) {
this.excessArguments(args.slice(expectedArgsCount));
}

// Add the options (which might be the command, with options as properties!)
if (this._passCommandToAction) {
actionArgs[expectedArgsCount] = this;
} else {
actionArgs[expectedArgsCount] = this.opts();
}
// Add the extra arguments so available too.
if (args.length > expectedArgsCount) {
actionArgs.push(args.slice(expectedArgsCount));
}
// Add command
actionArgs.push(this);

const actionResult = fn.apply(this, actionArgs);
// Remember result in case it is async. Assume parseAsync getting called on root.
Expand Down Expand Up @@ -1161,6 +1164,17 @@ Read more on https://git.io/JJc0W`);
return this;
};

/**
* Allow excess arguments on the command line, beyond what has been declared.
*
* @param {Boolean} [allowExcess] - if `true` or omitted, no error will be thrown
* for unknown arguments.
*/
allowExcessArguments(allowExcess) {
this._allowExcessArguments = (allowExcess === undefined) || !!allowExcess;
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 @@ -1741,6 +1755,20 @@ Read more on https://git.io/JJc0W`);
this._displayError(1, 'commander.unknownOption', message);
};

/**
* Excess arguments `arg`.
*
* @param {string[]} args
* @api private
*/

excessArguments(args) {
if (this._allowExcessArguments || args.length === 0) return;
if (this.name() === '*') return; // support legacy use, do not enforce arguments
const message = `error: excess argument${args.length > 1 ? 's' : ''} '${args.join(' ')}'`;
this._displayError(1, 'commander.excessArguments', message);
};

/**
* Unknown command.
*
Expand Down
8 changes: 4 additions & 4 deletions tests/args.variadic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('variadic argument', () => {

program.parse(['node', 'test', 'id']);

expect(actionMock).toHaveBeenCalledWith('id', [], program);
expect(actionMock).toHaveBeenCalledWith('id', [], program, program);
});

test('when extra arguments specified for program then variadic arg is array of values', () => {
Expand All @@ -25,7 +25,7 @@ describe('variadic argument', () => {

program.parse(['node', 'test', 'id', ...extraArguments]);

expect(actionMock).toHaveBeenCalledWith('id', extraArguments, program);
expect(actionMock).toHaveBeenCalledWith('id', extraArguments, program, program);
});

test('when no extra arguments specified for command then variadic arg is empty array', () => {
Expand All @@ -37,7 +37,7 @@ describe('variadic argument', () => {

program.parse(['node', 'test', 'sub']);

expect(actionMock).toHaveBeenCalledWith([], cmd);
expect(actionMock).toHaveBeenCalledWith([], cmd, cmd);
});

test('when extra arguments specified for command then variadic arg is array of values', () => {
Expand All @@ -50,7 +50,7 @@ describe('variadic argument', () => {

program.parse(['node', 'test', 'sub', ...extraArguments]);

expect(actionMock).toHaveBeenCalledWith(extraArguments, cmd);
expect(actionMock).toHaveBeenCalledWith(extraArguments, cmd, cmd);
});

test('when program variadic argument not last then error', () => {
Expand Down
41 changes: 28 additions & 13 deletions tests/command.action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test('when .action called then command passed to action', () => {
.command('info')
.action(actionMock);
program.parse(['node', 'test', 'info']);
expect(actionMock).toHaveBeenCalledWith(cmd);
expect(actionMock).toHaveBeenCalledWith(cmd, cmd);
});

test('when .action called then program.args only contains args', () => {
Expand All @@ -23,16 +23,31 @@ test('when .action called then program.args only contains args', () => {
expect(program.args).toEqual(['info', 'my-file']);
});

test('when .action called with extra arguments then extras also passed to action', () => {
// This is a new and undocumented behaviour for now.
// Might make this an error by default in future.
test('when .action called with excess arguments then error', () => {
const actionMock = jest.fn();
const program = new commander.Command();
const cmd = program
program.exitOverride();
program
.command('info <file>')
.configureOutput({ writeErr: () => {} })
.action(actionMock);
program.parse(['node', 'test', 'info', 'my-file', 'a']);
expect(actionMock).toHaveBeenCalledWith('my-file', cmd, ['a']);
expect(() => {
program.parse(['node', 'test', 'info', 'my-file', 'a']);
}).toThrow();
});

test('when .action called with allowed excess arguments then no error', () => {
const actionMock = jest.fn();
const program = new commander.Command();
program.exitOverride();
program
.command('info <file>')
.allowExcessArguments()
.action(actionMock);
expect(() => {
program.parse(['node', 'test', 'info', 'my-file', 'a']);
}).not.toThrow();
expect(actionMock).toHaveBeenCalled();
});

test('when .action on program with required argument and argument supplied then action called', () => {
Expand All @@ -42,7 +57,7 @@ test('when .action on program with required argument and argument supplied then
.arguments('<file>')
.action(actionMock);
program.parse(['node', 'test', 'my-file']);
expect(actionMock).toHaveBeenCalledWith('my-file', program);
expect(actionMock).toHaveBeenCalledWith('my-file', program, program);
});

test('when .action on program with required argument and argument not supplied then action not called', () => {
Expand All @@ -66,7 +81,7 @@ test('when .action on program and no arguments then action called', () => {
program
.action(actionMock);
program.parse(['node', 'test']);
expect(actionMock).toHaveBeenCalledWith(program);
expect(actionMock).toHaveBeenCalledWith(program, program);
});

test('when .action on program with optional argument supplied then action called', () => {
Expand All @@ -76,7 +91,7 @@ test('when .action on program with optional argument supplied then action called
.arguments('[file]')
.action(actionMock);
program.parse(['node', 'test', 'my-file']);
expect(actionMock).toHaveBeenCalledWith('my-file', program);
expect(actionMock).toHaveBeenCalledWith('my-file', program, program);
});

test('when .action on program without optional argument supplied then action called', () => {
Expand All @@ -86,7 +101,7 @@ test('when .action on program without optional argument supplied then action cal
.arguments('[file]')
.action(actionMock);
program.parse(['node', 'test']);
expect(actionMock).toHaveBeenCalledWith(undefined, program);
expect(actionMock).toHaveBeenCalledWith(undefined, program, program);
});

test('when .action on program with optional argument and subcommand and program argument then program action called', () => {
Expand All @@ -100,7 +115,7 @@ test('when .action on program with optional argument and subcommand and program

program.parse(['node', 'test', 'a']);

expect(actionMock).toHaveBeenCalledWith('a', program);
expect(actionMock).toHaveBeenCalledWith('a', program, program);
});

// Changes made in #1062 to allow this case
Expand All @@ -115,7 +130,7 @@ test('when .action on program with optional argument and subcommand and no progr

program.parse(['node', 'test']);

expect(actionMock).toHaveBeenCalledWith(undefined, program);
expect(actionMock).toHaveBeenCalledWith(undefined, program, program);
});

test('when action is async then can await parseAsync', async() => {
Expand Down
1 change: 1 addition & 0 deletions tests/command.allowUnknownOption.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('allowUnknownOption', () => {
.exitOverride()
.command('sub')
.allowUnknownOption()
.arguments('<args...>')
.option('-p, --pepper', 'add pepper')
.action(() => { });

Expand Down
2 changes: 2 additions & 0 deletions tests/command.default.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('default action command', () => {
program
.command('default', { isDefault: true })
.allowUnknownOption()
.allowExcessArguments()
.action(actionMock);
return { program, actionMock };
}
Expand Down Expand Up @@ -62,6 +63,7 @@ describe('default added command', () => {
const actionMock = jest.fn();
const defaultCmd = new commander.Command('default')
.allowUnknownOption()
.allowExcessArguments()
.action(actionMock);

const program = new commander.Command();
Expand Down
18 changes: 18 additions & 0 deletions tests/command.exitOverride.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,24 @@ describe('.exitOverride and error details', () => {
expectCommanderError(caughtErr, 1, 'commander.unknownOption', "error: unknown option '-m'");
});

test('when specify excess program arguments then throw CommanderError', () => {
const program = new commander.Command();
program
.arguments('<file>')
.exitOverride()
.action(() => {});

let caughtErr;
try {
program.parse(['node', 'test', 'expected', 'excess']);
} catch (err) {
caughtErr = err;
}

expect(stderrSpy).toHaveBeenCalled();
expectCommanderError(caughtErr, 1, 'commander.excessArguments', "error: excess argument 'excess'");
});

test('when specify unknown command then throw CommanderError', () => {
const program = new commander.Command();
program
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 @@ -24,7 +24,7 @@ describe('unknownOption', () => {
}).not.toThrow();
});

test('when unknown command but action handler then no error', () => {
test('when unknown command and program action handler then error due to excess argument', () => {
const program = new commander.Command();
program
.exitOverride()
Expand All @@ -33,7 +33,7 @@ describe('unknownOption', () => {
.action(() => { });
expect(() => {
program.parse('node test.js unknown'.split(' '));
}).not.toThrow();
}).toThrow();
});

test('when unknown command but listener then no error', () => {
Expand Down
8 changes: 4 additions & 4 deletions tests/commander.configureCommand.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ test('when default then command passed to action', () => {
.arguments('<value>')
.action(callback);
program.parse(['node', 'test', 'value']);
expect(callback).toHaveBeenCalledWith('value', program);
expect(callback).toHaveBeenCalledWith('value', program, program);
});

// storeOptionsAsProperties
Expand Down Expand Up @@ -61,7 +61,7 @@ test('when passCommandToAction() then command passed to action', () => {
.arguments('<value>')
.action(callback);
program.parse(['node', 'test', 'value']);
expect(callback).toHaveBeenCalledWith('value', program);
expect(callback).toHaveBeenCalledWith('value', program, program);
});

test('when passCommandToAction(true) then command passed to action', () => {
Expand All @@ -72,7 +72,7 @@ test('when passCommandToAction(true) then command passed to action', () => {
.arguments('<value>')
.action(callback);
program.parse(['node', 'test', 'value']);
expect(callback).toHaveBeenCalledWith('value', program);
expect(callback).toHaveBeenCalledWith('value', program, program);
});

test('when passCommandToAction(false) then options passed to action', () => {
Expand All @@ -83,5 +83,5 @@ test('when passCommandToAction(false) then options passed to action', () => {
.arguments('<value>')
.action(callback);
program.parse(['node', 'test', 'value']);
expect(callback).toHaveBeenCalledWith('value', program.opts());
expect(callback).toHaveBeenCalledWith('value', program.opts(), program);
});