Skip to content

Commit

Permalink
Merge pull request #1467 from Niryo/addArgument
Browse files Browse the repository at this point in the history
added addArguments method
  • Loading branch information
shadowspawn committed Mar 27, 2021
2 parents ad20e0b + 4a8e323 commit 9a77be4
Show file tree
Hide file tree
Showing 9 changed files with 257 additions and 105 deletions.
29 changes: 18 additions & 11 deletions Readme.md
Expand Up @@ -428,27 +428,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 @@ -464,7 +472,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 @@ -474,7 +481,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')
.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
144 changes: 84 additions & 60 deletions index.js
Expand Up @@ -28,11 +28,11 @@ class Help {
const visibleCommands = cmd.commands.filter(cmd => !cmd._hidden);
if (cmd._hasImplicitHelpCommand()) {
// Create a command matching the implicit help command.
const args = cmd._helpCommandnameAndArgs.split(/ +/);
const helpCommand = cmd.createCommand(args.shift())
const [, helpName, helpArgs] = cmd._helpCommandnameAndArgs.match(/([^ ]+) *(.*)/);
const helpCommand = cmd.createCommand(helpName)
.helpOption(false);
helpCommand.description(cmd._helpCommandDescription);
helpCommand._parseExpectedArgs(args);
if (helpArgs) helpCommand.arguments(helpArgs);
visibleCommands.push(helpCommand);
}
if (this.sortSubcommands) {
Expand Down Expand Up @@ -86,11 +86,15 @@ class Help {
*/

visibleArguments(cmd) {
if (cmd._argsDescription && cmd._args.length) {
return cmd._args.map((argument) => {
return { term: argument.name, description: cmd._argsDescription[argument.name] || '' };
}, 0);
}
// If there are some argument description then return all the arguments.
if (cmd._argsDescription || cmd._args.find(argument => argument.description)) {
const legacyDescriptions = cmd._argsDescription || {};
return cmd._args.map(argument => {
const term = argument.name;
const description = argument.description || legacyDescriptions[argument.name] || '';
return { term, description };
});
};
return [];
}

Expand Down Expand Up @@ -344,6 +348,41 @@ class Help {
}
}

class Argument {
/**
* Initialize a new argument with the given detail and description.
*
* @param {string} detail
* @param {string} [description]
*/

constructor(detail, description) {
this.required = false;
this.variadic = false;
this.name = '';
this.description = description || '';

switch (detail[0]) {
case '<': // e.g. <required>
this.required = true;
this.name = detail.slice(1, -1);
break;
case '[': // e.g. [optional]
this.name = detail.slice(1, -1);
break;
}

if (this.name.length > 3 && this.name.slice(-3) === '...') {
this.variadic = true;
this.name = this.name.slice(0, -3);
}

if (this.name.length === 0) {
throw new Error(`Unrecognised argument format (expecting '<required>' or '[optional]'): ${detail}`);
}
}
}

class Option {
/**
* Initialize a new `Option` with the given `flags` and `description`.
Expand Down Expand Up @@ -566,7 +605,7 @@ class Command extends EventEmitter {
this._aliases = [];
this._combineFlagAndOptionalValue = true;
this._description = '';
this._argsDescription = undefined;
this._argsDescription = undefined; // legacy
this._enablePositionalOptions = false;
this._passThroughOptions = false;

Expand Down Expand Up @@ -626,8 +665,8 @@ class Command extends EventEmitter {
desc = null;
}
opts = opts || {};
const args = nameAndArgs.split(/ +/);
const cmd = this.createCommand(args.shift());
const [, name, args] = nameAndArgs.match(/([^ ]+) *(.*)/);
const cmd = this.createCommand(name);

if (desc) {
cmd.description(desc);
Expand All @@ -654,8 +693,8 @@ class Command extends EventEmitter {
cmd._enablePositionalOptions = this._enablePositionalOptions;

cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor
if (args) cmd.arguments(args);
this.commands.push(cmd);
cmd._parseExpectedArgs(args);
cmd.parent = this;

if (desc) return this;
Expand Down Expand Up @@ -766,10 +805,37 @@ class Command extends EventEmitter {
* Define argument syntax for the command.
*/

arguments(desc) {
return this._parseExpectedArgs(desc.split(/ +/));
arguments(details) {
details.split(/ +/).forEach((detail) => {
this.argument(detail);
});
return this;
};

/**
* Define argument syntax for the command.
* @param {Argument} argument
*/
addArgument(argument) {
const previousArgument = this._args.slice(-1)[0];
if (previousArgument && previousArgument.variadic) {
throw new Error(`only the last argument can be variadic '${previousArgument.name}'`);
}
this._args.push(argument);
return this;
}

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

/**
* Override default decision whether to add implicit help command.
*
Expand Down Expand Up @@ -806,51 +872,6 @@ class Command extends EventEmitter {
return this._addImplicitHelpCommand;
};

/**
* Parse expected `args`.
*
* For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
*
* @param {Array} args
* @return {Command} `this` command for chaining
* @api private
*/

_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);
}
});
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 @@ -1851,7 +1872,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 @@ -2096,6 +2119,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
7 changes: 7 additions & 0 deletions tests/args.badArg.test.js
@@ -0,0 +1,7 @@
const commander = require('../');

test('should throw on bad argument', () => {
const program = new commander.Command();
expect(() => program.addArgument(new commander.Argument('bad-format'))).toThrow();
expect(() => program.argument('bad-format')).toThrow();
});
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...]');
});
});

0 comments on commit 9a77be4

Please sign in to comment.