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

added addArguments method #1467

Merged
merged 10 commits into from
Mar 27, 2021
46 changes: 37 additions & 9 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,27 +412,35 @@ subcommand is specified ([example](./examples/defaultCommand.js)).

### Specify the argument syntax

You use `.arguments` to specify the expected command-arguments for the top-level command, and for subcommands they are usually
You use `.argument` to specify the expected command-arguments for the top-level command, and for subcommands they are usually
included in the `.command` call. Angled brackets (e.g. `<required>`) indicate required command-arguments.
Square brackets (e.g. `[optional]`) indicate optional command-arguments.
You can optionally describe the arguments in the help by supplying a hash as second parameter to `.description()`.

Example file: [arguments.js](./examples/arguments.js)

```js
program
.version('0.1.0')
.arguments('<username> [password]')
.description('test command', {
username: 'user to login',
password: 'password for user, if required'
})
.argument('<username>', 'user to login')
.argument('[password]', 'password for user, if required')
.action((username, password) => {
console.log('username:', username);
console.log('environment:', password || 'no password given');
});
```

For more complex cases, use `.addArgument()` and pass `Argument` constructor.
```js
program
.version('0.1.0')
.addArgument(new Argument('<username>', 'user to login'))
.action((username) => {
console.log('username:', username);
});
```


Example file: [argument.js](./examples/argument.js)


The last argument of a command can be variadic, and only the last argument. To make an argument variadic you
append `...` to the argument name. For example:

Expand All @@ -449,6 +457,26 @@ program

The variadic argument is passed to the action handler as an array.


You can supply all the arguments at once using `.arguments()`, and optionally
describe the arguments in the help by supplying a hash as second parameter to `.description()`.

Example file: [arguments.js](./examples/arguments.js)

```js
program
.version('0.1.0')
.arguments('<username> [password]')
.description('test command', {
username: 'user to login',
password: 'password for user, if required'
})
.action((username, password) => {
console.log('username:', username);
console.log('environment:', password || 'no password given');
});
```

shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
### Action handler

The action handler gets passed a parameter for each command-argument you declared, and two additional parameters
Expand Down
4 changes: 2 additions & 2 deletions examples/advanced-argument.js → examples/argument.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node

// This example shows specifying the arguments using addArgument() function.
// This example shows specifying the arguments using addArgument() and argument() function.

// const { Command } = require('commander'); // (normal include)
const { Command, Argument } = require('../'); // include commander in git clone of commander repo
Expand All @@ -9,7 +9,7 @@ const program = new Command();
program
.version('0.1.0')
.addArgument(new Argument('<username>', 'user to login'))
.addArgument(new Argument('[password]', 'password'))
.argument('[password]', 'password')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description from arguments.js would be better, 'password for user, if required'.

.description('test command')
.action((username, password) => {
console.log('username:', username);
Expand Down
11 changes: 11 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,17 @@ class Command extends EventEmitter {
return this;
}

/**
* Define argument syntax for the command
* @param {string} arg
* @param {object} [description]
*/
argument(arg, description) {
const argument = new Argument(arg, description);
this.addArgument(argument);
return this;
}

/**
* Override default decision whether to add implicit help command.
*
Expand Down
84 changes: 84 additions & 0 deletions tests/argument.variadic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const commander = require('../');

// Testing variadic arguments. Testing all the action arguments, but could test just variadicArg.

describe('variadic argument', () => {
test('when no extra arguments specified for program then variadic arg is empty array', () => {
const actionMock = jest.fn();
const program = new commander.Command();
program
.argument('<id>')
.argument('[variadicArg...]')
.action(actionMock);

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

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

test('when extra arguments specified for program then variadic arg is array of values', () => {
const actionMock = jest.fn();
const program = new commander.Command();
program
.addArgument(new commander.Argument('<id>'))
.argument('[variadicArg...]')
.action(actionMock);
const extraArguments = ['a', 'b', 'c'];

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

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

test('when no extra arguments specified for command then variadic arg is empty array', () => {
const actionMock = jest.fn();
const program = new commander.Command();
const cmd = program
.command('sub [variadicArg...]')
.action(actionMock);

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

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

test('when extra arguments specified for command then variadic arg is array of values', () => {
const actionMock = jest.fn();
const program = new commander.Command();
const cmd = program
.command('sub [variadicArg...]')
.action(actionMock);
const extraArguments = ['a', 'b', 'c'];

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

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

test('when program variadic argument not last then error', () => {
const program = new commander.Command();

expect(() => {
program
.argument('<variadicArg...>')
.argument('[optionalArg]');
}).toThrow("only the last argument can be variadic 'variadicArg'");
});

test('when command variadic argument not last then error', () => {
const program = new commander.Command();

expect(() => {
program.command('sub <variadicArg...> [optionalArg]');
}).toThrow("only the last argument can be variadic 'variadicArg'");
});

test('when variadic argument then usage shows variadic', () => {
const program = new commander.Command();
program
.name('foo')
.argument('[args...]');

expect(program.usage()).toBe('[options] [args...]');
});
});
28 changes: 20 additions & 8 deletions tests/command.action.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ test('when .action on program with required argument and argument supplied then
const program = new commander.Command();
program
.arguments('<file>')
.argument('<second>')
.addArgument(new commander.Argument('<last>'))
.action(actionMock);
program.parse(['node', 'test', 'my-file']);
expect(actionMock).toHaveBeenCalledWith('my-file', program.opts(), program);
program.parse(['node', 'test', 'my-file', 'second', 'last']);
expect(actionMock).toHaveBeenCalledWith('my-file', 'second', 'last', program.opts(), program);
});

test('when .action on program with required argument and argument not supplied then action not called', () => {
Expand All @@ -40,6 +42,8 @@ test('when .action on program with required argument and argument not supplied t
.exitOverride()
.configureOutput({ writeErr: () => {} })
.arguments('<file>')
.argument('<second>')
.addArgument(new commander.Argument('<last>'))
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
.action(actionMock);
expect(() => {
program.parse(['node', 'test']);
Expand All @@ -62,33 +66,39 @@ test('when .action on program with optional argument supplied then action called
const program = new commander.Command();
program
.arguments('[file]')
.argument('[second]')
.addArgument(new commander.Argument('[last]'))
.action(actionMock);
program.parse(['node', 'test', 'my-file']);
expect(actionMock).toHaveBeenCalledWith('my-file', program.opts(), program);
program.parse(['node', 'test', 'my-file', 'second', 'last']);
expect(actionMock).toHaveBeenCalledWith('my-file', 'second', 'last', program.opts(), program);
});

test('when .action on program without optional argument supplied then action called', () => {
const actionMock = jest.fn();
const program = new commander.Command();
program
.arguments('[file]')
.argument('[second]')
.addArgument(new commander.Argument('[last]'))
.action(actionMock);
program.parse(['node', 'test']);
expect(actionMock).toHaveBeenCalledWith(undefined, program.opts(), program);
expect(actionMock).toHaveBeenCalledWith(undefined, undefined, undefined, program.opts(), program);
});

test('when .action on program with optional argument and subcommand and program argument then program action called', () => {
const actionMock = jest.fn();
const program = new commander.Command();
program
.arguments('[file]')
.argument('[second]')
.addArgument(new commander.Argument('[last]'))
.action(actionMock);
program
.command('subcommand');

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

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

// Changes made in #1062 to allow this case
Expand All @@ -97,13 +107,15 @@ test('when .action on program with optional argument and subcommand and no progr
const program = new commander.Command();
program
.arguments('[file]')
.argument('[second]')
.addArgument(new commander.Argument('[last]'))
.action(actionMock);
program
.command('subcommand');

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

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

test('when action is async then can await parseAsync', async() => {
Expand Down
6 changes: 4 additions & 2 deletions tests/command.usage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ test('when arguments then arguments included in usage', () => {
const program = new commander.Command();

program
.arguments('<file>');
.arguments('<file>')
.argument('<secondFile>')
.addArgument(new commander.Argument('<lastFile>'));

expect(program.usage()).toMatch('<file>');
expect(program.usage()).toMatch('<file> <secondFile> <lastFile>');
});

test('when options and command and arguments then all three included in usage', () => {
Expand Down
6 changes: 4 additions & 2 deletions tests/help.commandUsage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,10 @@ describe('commandUsage', () => {
const program = new commander.Command();
program
.name('program')
.arguments('<file>');
.arguments('<file>')
.argument('<desc>', 'description')
.addArgument(new commander.Argument('<info>', 'info'));
const helper = new commander.Help();
expect(helper.commandUsage(program)).toEqual('program [options] <file>');
expect(helper.commandUsage(program)).toEqual('program [options] <file> <desc> <info>');
});
});
13 changes: 13 additions & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ declare namespace commander {
}
type InvalidOptionArgumentErrorConstructor = new (message: string) => InvalidOptionArgumentError;

interface Argument {
description: string;
argDetails: {
required: boolean;
name: string;
variadic: boolean;
};
}

interface Option {
flags: string;
description: string;
Expand Down Expand Up @@ -80,6 +89,7 @@ declare namespace commander {
name(): string;
}
type OptionConstructor = new (flags: string, description?: string) => Option;
type ArgumentConstructor = new (arg: string, description?: string) => Argument;

interface Help {
/** output helpWidth, long lines are wrapped to fit */
Expand Down Expand Up @@ -233,6 +243,8 @@ declare namespace commander {
* @returns `this` command for chaining
*/
arguments(desc: string): this;
argument(arg: string, description: string): this;
addArgument(arg: Argument): this;

/**
* Override default decision whether to add implicit help command.
Expand Down Expand Up @@ -608,6 +620,7 @@ declare namespace commander {
program: Command;
Command: CommandConstructor;
Option: OptionConstructor;
Argument: ArgumentConstructor;
CommanderError: CommanderErrorConstructor;
InvalidOptionArgumentError: InvalidOptionArgumentErrorConstructor;
Help: HelpConstructor;
Expand Down