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
29 changes: 18 additions & 11 deletions Readme.md
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 @@ -448,7 +456,6 @@ program
```

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

### Action handler

The action handler gets passed a parameter for each command-argument you declared, and two additional parameters
Expand All @@ -458,7 +465,7 @@ Example file: [thank.js](./examples/thank.js)

```js
program
.arguments('<name>')
.argument('<name>')
.option('-t, --title <honorific>', 'title to use before name')
.option('-d, --debug', 'display some debugging')
.action((name, options, command) => {
Expand Down
24 changes: 24 additions & 0 deletions examples/argument.js
@@ -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
106 changes: 80 additions & 26 deletions index.js
Expand Up @@ -344,6 +344,23 @@ class Help {
}
}

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

constructor(arg, description) {
const argDetails = parseArg(arg);
this.name = argDetails.name;
this.required = argDetails.required;
this.variadic = argDetails.variadic;
this.description = description || '';
}
}

class Option {
/**
* Initialize a new `Option` with the given `flags` and `description`.
Expand Down Expand Up @@ -755,6 +772,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);
if (!this._argsDescription) {
this._argsDescription = {};
}
this._argsDescription[argument.name] = argument.description;
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 +846,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 +1859,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 +2106,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 +2226,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
@@ -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...]');
});
});