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
24 changes: 24 additions & 0 deletions examples/argument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env node

// 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
const program = new Command();

program
.version('0.1.0')
.addArgument(new Argument('<username>', 'user to login'))
.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);
console.log('environment:', password || 'no password given');
});

program.parse();

// Try the following:
// node arguments.js --help
// node arguments.js user
// node arguments.js user secret
103 changes: 77 additions & 26 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,20 @@ class Help {
}
}

class Argument {
/**
* Initialize a new argument with description.
*
* @param {string} arg
* @param {object} [description]
*/

constructor(arg, description) {
this.argDetails = parseArg(arg);
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
this.description = description || '';
}
}

class Option {
/**
* Initialize a new `Option` with the given `flags` and `description`.
Expand Down Expand Up @@ -755,6 +769,31 @@ class Command extends EventEmitter {
return this._parseExpectedArgs(desc.split(/ +/));
};

/**
* Define argument syntax for the command.
* @param {Argument} argument
*/
addArgument(argument) {
this._args.push(argument.argDetails);
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
if (!this._argsDescription) {
this._argsDescription = {};
}
this._argsDescription[argument.argDetails.name] = argument.description;
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
this._validateArgs();
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 Expand Up @@ -804,37 +843,20 @@ class Command extends EventEmitter {
_parseExpectedArgs(args) {
if (!args.length) return;
args.forEach((arg) => {
const argDetails = {
required: false,
name: '',
variadic: false
};

switch (arg[0]) {
case '<':
argDetails.required = true;
argDetails.name = arg.slice(1, -1);
break;
case '[':
argDetails.name = arg.slice(1, -1);
break;
}

if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
argDetails.variadic = true;
argDetails.name = argDetails.name.slice(0, -3);
}
if (argDetails.name) {
this._args.push(argDetails);
}
const argDetails = parseArg(arg);
argDetails && this._args.push(argDetails);
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
});
this._validateArgs();
return this;
};

_validateArgs() {
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
this._args.forEach((arg, i) => {
if (arg.variadic && i < this._args.length - 1) {
throw new Error(`only the last argument can be variadic '${arg.name}'`);
}
});
return this;
};
}

/**
* Register callback to use as replacement for calling process.exit.
Expand Down Expand Up @@ -1834,7 +1856,9 @@ class Command extends EventEmitter {
description(str, argsDescription) {
if (str === undefined && argsDescription === undefined) return this._description;
this._description = str;
this._argsDescription = argsDescription;
if (argsDescription) {
this._argsDescription = argsDescription;
}
return this;
};

Expand Down Expand Up @@ -2079,6 +2103,7 @@ exports.program = exports; // More explicit access to global command.

exports.Command = Command;
exports.Option = Option;
exports.Argument = Argument;
exports.CommanderError = CommanderError;
exports.InvalidOptionArgumentError = InvalidOptionArgumentError;
exports.Help = Help;
Expand Down Expand Up @@ -2198,3 +2223,29 @@ function incrementNodeInspectorPort(args) {
return arg;
});
}

function parseArg(arg) {
shadowspawn marked this conversation as resolved.
Show resolved Hide resolved
const argDetails = {
required: false,
name: '',
variadic: false
};

switch (arg[0]) {
case '<':
argDetails.required = true;
argDetails.name = arg.slice(1, -1);
break;
case '[':
argDetails.name = arg.slice(1, -1);
break;
}

if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
argDetails.variadic = true;
argDetails.name = argDetails.name.slice(0, -3);
}
if (argDetails.name) {
return argDetails;
}
}
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...]');
});
});