Skip to content

Commit

Permalink
Merge pull request #1 from shadowspawn/Niryo-addArgument
Browse files Browse the repository at this point in the history
  • Loading branch information
Niryo committed Mar 14, 2021
2 parents d106ca0 + 32f001b commit 4a8e323
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 100 deletions.
143 changes: 54 additions & 89 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 @@ -346,22 +350,35 @@ class Help {

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

constructor(arg, description) {
const argDetails = parseArg(arg);
if (argDetails === undefined) {
throw new Error(`Bad argument format: ${arg}`);
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 (argDetails) {
this.name = argDetails.name;
this.required = argDetails.required;
this.variadic = argDetails.variadic;
this.description = description || '';

if (this.name.length === 0) {
throw new Error(`Unrecognised argument format (expecting '<required>' or '[optional]'): ${detail}`);
}
}
}
Expand Down Expand Up @@ -573,7 +590,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 @@ -633,8 +650,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 @@ -661,8 +678,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 @@ -773,31 +790,33 @@ 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) {
this._args.push(argument);
if (!this._argsDescription) {
this._argsDescription = {};
const previousArgument = this._args.slice(-1)[0];
if (previousArgument && previousArgument.variadic) {
throw new Error(`only the last argument can be variadic '${previousArgument.name}'`);
}
this._argsDescription[argument.name] = argument.description;
this._validateArgs();
this._args.push(argument);
return this;
}

/**
* Define argument syntax for the command
* @param {string} arg
* @param {object} [description]
* @param {string} detail
* @param {string} [description]
*/
argument(arg, description) {
const argument = new Argument(arg, description);
argument(detail, description) {
const argument = new Argument(detail, description);
this.addArgument(argument);
return this;
}
Expand Down Expand Up @@ -838,34 +857,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 = parseArg(arg);
argDetails && this._args.push(argDetails);
});
this._validateArgs();
return this;
};

_validateArgs() {
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}'`);
}
});
}

/**
* Register callback to use as replacement for calling process.exit.
*
Expand Down Expand Up @@ -2231,29 +2222,3 @@ function incrementNodeInspectorPort(args) {
return arg;
});
}

function parseArg(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) {
return argDetails;
}
}
4 changes: 2 additions & 2 deletions tests/args.badArg.test.js
Expand Up @@ -2,6 +2,6 @@ const commander = require('../');

test('should throw on bad argument', () => {
const program = new commander.Command();
expect(() => program.addArgument(new commander.Argument('bad name'))).toThrowError('Bad argument format: bad name');
expect(() => program.argument(new commander.Argument('bad name'))).toThrowError('Bad argument format: bad name');
expect(() => program.addArgument(new commander.Argument('bad-format'))).toThrow();
expect(() => program.argument('bad-format')).toThrow();
});
14 changes: 7 additions & 7 deletions tests/command.action.test.js
Expand Up @@ -23,14 +23,14 @@ test('when .action called then program.args only contains args', () => {
expect(program.args).toEqual(['info', 'my-file']);
});

test.each(getTestCases('<file>'))('when .action on program with required argument and argument supplied then action called', (program) => {
test.each(getTestCases('<file>'))('when .action on program with required argument via %s and argument supplied then action called', (methodName, program) => {
const actionMock = jest.fn();
program.action(actionMock);
program.parse(['node', 'test', 'my-file']);
expect(actionMock).toHaveBeenCalledWith('my-file', program.opts(), program);
});

test.each(getTestCases('<file>'))('when .action on program with required argument and argument not supplied then action not called', (program) => {
test.each(getTestCases('<file>'))('when .action on program with required argument via %s and argument not supplied then action not called', (methodName, program) => {
const actionMock = jest.fn();
program
.exitOverride()
Expand All @@ -52,21 +52,21 @@ test('when .action on program and no arguments then action called', () => {
expect(actionMock).toHaveBeenCalledWith(program.opts(), program);
});

test.each(getTestCases('[file]'))('when .action on program with optional argument supplied then action called', (program) => {
test.each(getTestCases('[file]'))('when .action on program with optional argument via %s supplied then action called', (methodName, program) => {
const actionMock = jest.fn();
program.action(actionMock);
program.parse(['node', 'test', 'my-file']);
expect(actionMock).toHaveBeenCalledWith('my-file', program.opts(), program);
});

test.each(getTestCases('[file]'))('when .action on program without optional argument supplied then action called', (program) => {
test.each(getTestCases('[file]'))('when .action on program without optional argument supplied then action called', (methodName, program) => {
const actionMock = jest.fn();
program.action(actionMock);
program.parse(['node', 'test']);
expect(actionMock).toHaveBeenCalledWith(undefined, program.opts(), program);
});

test.each(getTestCases('[file]'))('when .action on program with optional argument and subcommand and program argument then program action called', (program) => {
test.each(getTestCases('[file]'))('when .action on program with optional argument via %s and subcommand and program argument then program action called', (methodName, program) => {
const actionMock = jest.fn();
program.action(actionMock);
program
Expand All @@ -78,7 +78,7 @@ test.each(getTestCases('[file]'))('when .action on program with optional argumen
});

// Changes made in #1062 to allow this case
test.each(getTestCases('[file]'))('when .action on program with optional argument and subcommand and no program argument then program action called', (program) => {
test.each(getTestCases('[file]'))('when .action on program with optional argument via %s and subcommand and no program argument then program action called', (methodName, program) => {
const actionMock = jest.fn();
program.action(actionMock);
program.command('subcommand');
Expand Down Expand Up @@ -108,5 +108,5 @@ function getTestCases(arg) {
const withArguments = new commander.Command().arguments(arg);
const withArgument = new commander.Command().argument(arg);
const withAddArgument = new commander.Command().addArgument(new commander.Argument(arg));
return [withArguments, withArgument, withAddArgument];
return [['.arguments', withArguments], ['.argument', withArgument], ['.addArgument', withAddArgument]];
}
4 changes: 2 additions & 2 deletions tests/help.visibleCommands.test.js
Expand Up @@ -23,9 +23,9 @@ describe('visibleCommands', () => {
const program = new commander.Command();
program
.command('visible', 'desc')
.command('invisible executable', 'desc', { hidden: true });
.command('invisible-executable', 'desc', { hidden: true });
program
.command('invisible action', { hidden: true });
.command('invisible-action', { hidden: true });
const helper = new commander.Help();
const visibleCommandNames = helper.visibleCommands(program).map(cmd => cmd.name());
expect(visibleCommandNames).toEqual(['visible', 'help']);
Expand Down

0 comments on commit 4a8e323

Please sign in to comment.