From 39a47ed566a61032b84757c55a91011dfc10bd66 Mon Sep 17 00:00:00 2001 From: Nir Yosef Date: Sat, 6 Feb 2021 16:55:46 +0300 Subject: [PATCH 01/34] added addArguments method --- examples/advanced-argument.js | 24 +++++++++ index.js | 92 +++++++++++++++++++++++++---------- 2 files changed, 90 insertions(+), 26 deletions(-) create mode 100644 examples/advanced-argument.js diff --git a/examples/advanced-argument.js b/examples/advanced-argument.js new file mode 100644 index 000000000..4e9ee9ad2 --- /dev/null +++ b/examples/advanced-argument.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +// This example shows specifying the arguments using addArgument() 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('', 'user to login')) + .addArgument(new 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 diff --git a/index.js b/index.js index 20110ff11..ae5f36956 100644 --- a/index.js +++ b/index.js @@ -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); + this.description = description || ''; + } +} + class Option { /** * Initialize a new `Option` with the given `flags` and `description`. @@ -755,6 +769,20 @@ 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); + if (!this._argsDescription) { + this._argsDescription = {}; + } + this._argsDescription[argument.argDetails.name] = argument.description; + this._validateArgs(); + return this; + } + /** * Override default decision whether to add implicit help command. * @@ -804,37 +832,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); + 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}'`); } }); - return this; - }; + } /** * Register callback to use as replacement for calling process.exit. @@ -1834,7 +1845,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; }; @@ -2079,6 +2092,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; @@ -2198,3 +2212,29 @@ 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; + } +} From 88a4cc5ea5e1597f735c32d204107f6ff5586b80 Mon Sep 17 00:00:00 2001 From: Nir Yosef Date: Sat, 6 Feb 2021 17:09:43 +0300 Subject: [PATCH 02/34] fix undefined args --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index ae5f36956..5e7ba04fd 100644 --- a/index.js +++ b/index.js @@ -833,7 +833,7 @@ class Command extends EventEmitter { if (!args.length) return; args.forEach((arg) => { const argDetails = parseArg(arg); - this._args.push(argDetails); + argDetails && this._args.push(argDetails); }); this._validateArgs(); return this; From e8415ed2eb4a4692c298433386a01a3aa15a38e9 Mon Sep 17 00:00:00 2001 From: Nir Yosef Date: Sat, 13 Feb 2021 19:40:20 +0300 Subject: [PATCH 03/34] added tests, docs and typings --- Readme.md | 46 ++++++++-- .../{advanced-argument.js => argument.js} | 4 +- index.js | 11 +++ tests/argument.variadic.test.js | 84 +++++++++++++++++++ tests/command.action.test.js | 28 +++++-- tests/command.usage.test.js | 6 +- tests/help.commandUsage.test.js | 6 +- typings/index.d.ts | 13 +++ 8 files changed, 175 insertions(+), 23 deletions(-) rename examples/{advanced-argument.js => argument.js} (90%) create mode 100644 tests/argument.variadic.test.js diff --git a/Readme.md b/Readme.md index 94c3d8e47..866856b26 100644 --- a/Readme.md +++ b/Readme.md @@ -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. ``) 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(' [password]') - .description('test command', { - username: 'user to login', - password: 'password for user, if required' - }) + .argument('', '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('', '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: @@ -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(' [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'); + }); +``` + ### Action handler The action handler gets passed a parameter for each command-argument you declared, and two additional parameters diff --git a/examples/advanced-argument.js b/examples/argument.js similarity index 90% rename from examples/advanced-argument.js rename to examples/argument.js index 4e9ee9ad2..da5a299dd 100644 --- a/examples/advanced-argument.js +++ b/examples/argument.js @@ -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 @@ -9,7 +9,7 @@ const program = new Command(); program .version('0.1.0') .addArgument(new Argument('', 'user to login')) - .addArgument(new Argument('[password]', 'password')) + .argument('[password]', 'password') .description('test command') .action((username, password) => { console.log('username:', username); diff --git a/index.js b/index.js index 5e7ba04fd..8daf12de9 100644 --- a/index.js +++ b/index.js @@ -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. * diff --git a/tests/argument.variadic.test.js b/tests/argument.variadic.test.js new file mode 100644 index 000000000..e1c452b1d --- /dev/null +++ b/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('') + .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('')) + .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('') + .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 [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...]'); + }); +}); diff --git a/tests/command.action.test.js b/tests/command.action.test.js index b1f936f36..0f222d1ad 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -28,9 +28,11 @@ test('when .action on program with required argument and argument supplied then const program = new commander.Command(); program .arguments('') + .argument('') + .addArgument(new commander.Argument('')) .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', () => { @@ -40,6 +42,8 @@ test('when .action on program with required argument and argument not supplied t .exitOverride() .configureOutput({ writeErr: () => {} }) .arguments('') + .argument('') + .addArgument(new commander.Argument('')) .action(actionMock); expect(() => { program.parse(['node', 'test']); @@ -62,9 +66,11 @@ 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', () => { @@ -72,9 +78,11 @@ test('when .action on program without optional argument supplied then action cal 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', () => { @@ -82,13 +90,15 @@ test('when .action on program with optional argument and subcommand and program 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 @@ -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() => { diff --git a/tests/command.usage.test.js b/tests/command.usage.test.js index 8cae3ad9f..b3088fe8d 100644 --- a/tests/command.usage.test.js +++ b/tests/command.usage.test.js @@ -82,9 +82,11 @@ test('when arguments then arguments included in usage', () => { const program = new commander.Command(); program - .arguments(''); + .arguments('') + .argument('') + .addArgument(new commander.Argument('')); - expect(program.usage()).toMatch(''); + expect(program.usage()).toMatch(' '); }); test('when options and command and arguments then all three included in usage', () => { diff --git a/tests/help.commandUsage.test.js b/tests/help.commandUsage.test.js index abeaceba0..941b0f9b3 100644 --- a/tests/help.commandUsage.test.js +++ b/tests/help.commandUsage.test.js @@ -42,8 +42,10 @@ describe('commandUsage', () => { const program = new commander.Command(); program .name('program') - .arguments(''); + .arguments('') + .argument('', 'description') + .addArgument(new commander.Argument('', 'info')); const helper = new commander.Help(); - expect(helper.commandUsage(program)).toEqual('program [options] '); + expect(helper.commandUsage(program)).toEqual('program [options] '); }); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 36163d3f8..91d288563 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -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; @@ -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 */ @@ -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. @@ -608,6 +620,7 @@ declare namespace commander { program: Command; Command: CommandConstructor; Option: OptionConstructor; + Argument: ArgumentConstructor; CommanderError: CommanderErrorConstructor; InvalidOptionArgumentError: InvalidOptionArgumentErrorConstructor; Help: HelpConstructor; From 782f7055a1bb911a0f4e8cd4f061d28554b2e09a Mon Sep 17 00:00:00 2001 From: Nir Yosef Date: Fri, 19 Feb 2021 14:12:25 +0300 Subject: [PATCH 04/34] code review fixes --- Readme.md | 23 +---------- index.js | 9 +++-- tests/command.action.test.js | 77 +++++++++++++----------------------- 3 files changed, 34 insertions(+), 75 deletions(-) diff --git a/Readme.md b/Readme.md index 866856b26..c2389c964 100644 --- a/Readme.md +++ b/Readme.md @@ -456,27 +456,6 @@ 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(' [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'); - }); -``` - ### Action handler The action handler gets passed a parameter for each command-argument you declared, and two additional parameters @@ -486,7 +465,7 @@ Example file: [thank.js](./examples/thank.js) ```js program - .arguments('') + .argument('') .option('-t, --title ', 'title to use before name') .option('-d, --debug', 'display some debugging') .action((name, options, command) => { diff --git a/index.js b/index.js index 8daf12de9..90e65885e 100644 --- a/index.js +++ b/index.js @@ -353,7 +353,10 @@ class Argument { */ constructor(arg, description) { - this.argDetails = parseArg(arg); + const argDetails = parseArg(arg); + this.name = argDetails.name; + this.required = argDetails.required; + this.variadic = argDetails.variadic; this.description = description || ''; } } @@ -774,11 +777,11 @@ class Command extends EventEmitter { * @param {Argument} argument */ addArgument(argument) { - this._args.push(argument.argDetails); + this._args.push(argument); if (!this._argsDescription) { this._argsDescription = {}; } - this._argsDescription[argument.argDetails.name] = argument.description; + this._argsDescription[argument.name] = argument.description; this._validateArgs(); return this; } diff --git a/tests/command.action.test.js b/tests/command.action.test.js index 0f222d1ad..bf0d34a6b 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -23,27 +23,18 @@ test('when .action called then program.args only contains args', () => { expect(program.args).toEqual(['info', 'my-file']); }); -test('when .action on program with required argument and argument supplied then action called', () => { +test.each(getTestCases(''))('when .action on program with required argument and argument supplied then action called', (program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('') - .argument('') - .addArgument(new commander.Argument('')) - .action(actionMock); - program.parse(['node', 'test', 'my-file', 'second', 'last']); - expect(actionMock).toHaveBeenCalledWith('my-file', 'second', 'last', program.opts(), program); + program.action(actionMock); + program.parse(['node', 'test', 'my-file']); + expect(actionMock).toHaveBeenCalledWith('my-file', program.opts(), program); }); -test('when .action on program with required argument and argument not supplied then action not called', () => { +test.each(getTestCases(''))('when .action on program with required argument and argument not supplied then action not called', (program) => { const actionMock = jest.fn(); - const program = new commander.Command(); program .exitOverride() .configureOutput({ writeErr: () => {} }) - .arguments('') - .argument('') - .addArgument(new commander.Argument('')) .action(actionMock); expect(() => { program.parse(['node', 'test']); @@ -61,61 +52,40 @@ test('when .action on program and no arguments then action called', () => { expect(actionMock).toHaveBeenCalledWith(program.opts(), program); }); -test('when .action on program with optional argument supplied then action called', () => { +test.each(getTestCases('[file]'))('when .action on program with optional argument supplied then action called', (program) => { 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', 'my-file', 'second', 'last']); - expect(actionMock).toHaveBeenCalledWith('my-file', 'second', 'last', program.opts(), program); + program.action(actionMock); + program.parse(['node', 'test', 'my-file']); + expect(actionMock).toHaveBeenCalledWith('my-file', program.opts(), program); }); -test('when .action on program without optional argument supplied then action called', () => { +test.each(getTestCases('[file]'))('when .action on program without optional argument supplied then action called', (program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('[file]') - .argument('[second]') - .addArgument(new commander.Argument('[last]')) - .action(actionMock); + program.action(actionMock); program.parse(['node', 'test']); - expect(actionMock).toHaveBeenCalledWith(undefined, undefined, undefined, program.opts(), program); + expect(actionMock).toHaveBeenCalledWith(undefined, program.opts(), program); }); -test('when .action on program with optional argument and subcommand and program argument then program action called', () => { +test.each(getTestCases('[file]'))('when .action on program with optional argument and subcommand and program argument then program action called', (program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('[file]') - .argument('[second]') - .addArgument(new commander.Argument('[last]')) - .action(actionMock); + program.action(actionMock); program .command('subcommand'); - program.parse(['node', 'test', 'a', 'second', 'last']); + program.parse(['node', 'test', 'a']); - expect(actionMock).toHaveBeenCalledWith('a', 'second', 'last', program.opts(), program); + expect(actionMock).toHaveBeenCalledWith('a', program.opts(), program); }); // Changes made in #1062 to allow this case -test('when .action on program with optional argument and subcommand and no program argument then program action called', () => { +test.each(getTestCases('[file]'))('when .action on program with optional argument and subcommand and no program argument then program action called', (program) => { 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.action(actionMock); + program.command('subcommand'); program.parse(['node', 'test']); - expect(actionMock).toHaveBeenCalledWith(undefined, undefined, undefined, program.opts(), program); + expect(actionMock).toHaveBeenCalledWith(undefined, program.opts(), program); }); test('when action is async then can await parseAsync', async() => { @@ -133,3 +103,10 @@ test('when action is async then can await parseAsync', async() => { await later; expect(asyncFinished).toBe(true); }); + +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]; +} From d106ca05992442cecbc2936a31d879d09f95b3bb Mon Sep 17 00:00:00 2001 From: Nir Yosef Date: Sat, 6 Mar 2021 13:44:10 +0300 Subject: [PATCH 05/34] throw error on bad arg --- index.js | 13 +++++++++---- tests/args.badArg.test.js | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 tests/args.badArg.test.js diff --git a/index.js b/index.js index 90e65885e..2c560bca3 100644 --- a/index.js +++ b/index.js @@ -354,10 +354,15 @@ class Argument { constructor(arg, description) { const argDetails = parseArg(arg); - this.name = argDetails.name; - this.required = argDetails.required; - this.variadic = argDetails.variadic; - this.description = description || ''; + if (argDetails === undefined) { + throw new Error(`Bad argument format: ${arg}`); + } + if (argDetails) { + this.name = argDetails.name; + this.required = argDetails.required; + this.variadic = argDetails.variadic; + this.description = description || ''; + } } } diff --git a/tests/args.badArg.test.js b/tests/args.badArg.test.js new file mode 100644 index 000000000..0d0b61b1f --- /dev/null +++ b/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 name'))).toThrowError('Bad argument format: bad name'); + expect(() => program.argument(new commander.Argument('bad name'))).toThrowError('Bad argument format: bad name'); +}); From 39cb757330b4ff66edd44032be8a7dc8986fc1ad Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 13 Mar 2021 17:13:09 +1300 Subject: [PATCH 06/34] Parse command-argument details in constructor --- index.js | 131 +++++++++++------------------ tests/args.badArg.test.js | 4 +- tests/help.visibleCommands.test.js | 4 +- 3 files changed, 55 insertions(+), 84 deletions(-) diff --git a/index.js b/index.js index 2c560bca3..6772bf885 100644 --- a/index.js +++ b/index.js @@ -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 pieces = cmd._helpCommandnameAndArgs.split(/ +/); + const helpCommand = cmd.createCommand(pieces.shift()) .helpOption(false); helpCommand.description(cmd._helpCommandDescription); - helpCommand._parseExpectedArgs(args); + if (pieces.length > 0) helpCommand.arguments(pieces.join(' ')); visibleCommands.push(helpCommand); } if (this.sortSubcommands) { @@ -346,22 +346,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. + 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 '' or '[optional]'): ${detail}`); } } } @@ -662,7 +675,7 @@ class Command extends EventEmitter { cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor this.commands.push(cmd); - cmd._parseExpectedArgs(args); + if (args.length > 0) cmd.arguments(args.join(' ')); cmd.parent = this; if (desc) return this; @@ -773,8 +786,11 @@ 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; }; /** @@ -782,22 +798,31 @@ class Command extends EventEmitter { * @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); - if (!this._argsDescription) { - this._argsDescription = {}; + + // Legacy description handling + if (argument.description) { + if (!this._argsDescription) { + this._argsDescription = {}; + } + this._argsDescription[argument.name] = argument.description; } - this._argsDescription[argument.name] = argument.description; - this._validateArgs(); + 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; } @@ -838,34 +863,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. * @@ -2231,29 +2228,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; - } -} diff --git a/tests/args.badArg.test.js b/tests/args.badArg.test.js index 0d0b61b1f..3ec3e4a31 100644 --- a/tests/args.badArg.test.js +++ b/tests/args.badArg.test.js @@ -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(); }); diff --git a/tests/help.visibleCommands.test.js b/tests/help.visibleCommands.test.js index bc8aed7e0..3ea8cefcc 100644 --- a/tests/help.visibleCommands.test.js +++ b/tests/help.visibleCommands.test.js @@ -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']); From 8c4d8d0a415f4327387e29c25f263048bd71b1db Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 13 Mar 2021 18:38:53 +1300 Subject: [PATCH 07/34] Handle argument descriptions separately for legacy and new support --- index.js | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/index.js b/index.js index 6772bf885..3f3b7ba19 100644 --- a/index.js +++ b/index.js @@ -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 []; } @@ -586,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; @@ -802,17 +806,7 @@ class Command extends EventEmitter { if (previousArgument && previousArgument.variadic) { throw new Error(`only the last argument can be variadic '${previousArgument.name}'`); } - this._args.push(argument); - - // Legacy description handling - if (argument.description) { - if (!this._argsDescription) { - this._argsDescription = {}; - } - this._argsDescription[argument.name] = argument.description; - } - return this; } From c2e0a5a9284aaccc7edb84b817cd2fbbbf519b92 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 13 Mar 2021 21:31:32 +1300 Subject: [PATCH 08/34] Add text to distinguish test names with .each --- tests/command.action.test.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/command.action.test.js b/tests/command.action.test.js index bf0d34a6b..765dddf0c 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -23,14 +23,14 @@ test('when .action called then program.args only contains args', () => { expect(program.args).toEqual(['info', 'my-file']); }); -test.each(getTestCases(''))('when .action on program with required argument and argument supplied then action called', (program) => { +test.each(getTestCases(''))('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(''))('when .action on program with required argument and argument not supplied then action not called', (program) => { +test.each(getTestCases(''))('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() @@ -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 @@ -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'); @@ -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]]; } From 32f001bfdc8c592d637754144bb97d52849d1500 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 14 Mar 2021 19:02:03 +1300 Subject: [PATCH 09/34] Match nameAndArgs into two parts, rather than split on spaces. --- index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 3f3b7ba19..848240746 100644 --- a/index.js +++ b/index.js @@ -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 pieces = cmd._helpCommandnameAndArgs.split(/ +/); - const helpCommand = cmd.createCommand(pieces.shift()) + const [, helpName, helpArgs] = cmd._helpCommandnameAndArgs.match(/([^ ]+) *(.*)/); + const helpCommand = cmd.createCommand(helpName) .helpOption(false); helpCommand.description(cmd._helpCommandDescription); - if (pieces.length > 0) helpCommand.arguments(pieces.join(' ')); + if (helpArgs) helpCommand.arguments(helpArgs); visibleCommands.push(helpCommand); } if (this.sortSubcommands) { @@ -650,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); @@ -678,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); - if (args.length > 0) cmd.arguments(args.join(' ')); cmd.parent = this; if (desc) return this; From ad20e0bb2e4b4264e53b7f5ff57e223d2031458f Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 22 Mar 2021 19:40:40 +1300 Subject: [PATCH 10/34] Update release date post-release to be more accurate --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b55881b5..d0cfdca47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. -## [7.2.0] (2021-03-26) +## [7.2.0] (2021-03-22) ### Added From da374365d3ebfab76d92232148b75671ddcb7743 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 17:00:30 +1300 Subject: [PATCH 11/34] Fix test naming --- tests/args.badArg.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/args.badArg.test.js b/tests/args.badArg.test.js index 3ec3e4a31..bbea378e4 100644 --- a/tests/args.badArg.test.js +++ b/tests/args.badArg.test.js @@ -1,6 +1,6 @@ const commander = require('../'); -test('should throw on bad argument', () => { +test('when unexpected argument format then throw', () => { const program = new commander.Command(); expect(() => program.addArgument(new commander.Argument('bad-format'))).toThrow(); expect(() => program.argument('bad-format')).toThrow(); From 81248e2c4f0a9ca03c6d2275b37510fa8e6521ba Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 17:04:48 +1300 Subject: [PATCH 12/34] Simplify and tidy argument example --- examples/argument.js | 12 ++++++------ examples/arguments.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/argument.js b/examples/argument.js index da5a299dd..83c779eae 100644 --- a/examples/argument.js +++ b/examples/argument.js @@ -1,19 +1,19 @@ #!/usr/bin/env node -// This example shows specifying the arguments using addArgument() and argument() function. +// This example shows specifying the arguments using argument() function. // const { Command } = require('commander'); // (normal include) -const { Command, Argument } = require('../'); // include commander in git clone of commander repo +const { Command } = require('../'); // include commander in git clone of commander repo const program = new Command(); program .version('0.1.0') - .addArgument(new Argument('', 'user to login')) - .argument('[password]', 'password') - .description('test command') + .argument('', 'user to login') + .argument('[password]', 'password for user, if required') + .description('example program for argument') .action((username, password) => { console.log('username:', username); - console.log('environment:', password || 'no password given'); + console.log('password:', password || 'no password given'); }); program.parse(); diff --git a/examples/arguments.js b/examples/arguments.js index 48372c648..4aab3ca73 100755 --- a/examples/arguments.js +++ b/examples/arguments.js @@ -15,7 +15,7 @@ program }) .action((username, password) => { console.log('username:', username); - console.log('environment:', password || 'no password given'); + console.log('password:', password || 'no password given'); }); program.parse(); From 6052c14a75284f5757fe57f32a068cdc570e215a Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 17:10:02 +1300 Subject: [PATCH 13/34] Typing and typings test for .argument --- typings/index.d.ts | 2 +- typings/index.test-d.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/typings/index.d.ts b/typings/index.d.ts index d5ef605df..d79430204 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -249,7 +249,7 @@ declare namespace commander { * @returns `this` command for chaining */ arguments(desc: string): this; - argument(arg: string, description: string): this; + argument(arg: string, description?: string): this; addArgument(arg: Argument): this; /** diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index ca6ee0a7f..07b1f5571 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -37,6 +37,10 @@ expectType(program.command('exec', 'exec description', { isDe // addCommand expectType(program.addCommand(new commander.Command('abc'))); +// argument +expectType(program.argument('')); +expectType(program.argument('', 'description')); + // arguments expectType(program.arguments(' [env]')); From 0dd35478414c2392d090621fb4ea78de0cb1dade Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 17:45:05 +1300 Subject: [PATCH 14/34] Expand argument section to include existing multiple-argument approaches. --- Readme.md | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/Readme.md b/Readme.md index 38ba6f5eb..41eeee1cf 100644 --- a/Readme.md +++ b/Readme.md @@ -428,9 +428,10 @@ subcommand is specified ([example](./examples/defaultCommand.js)). ### Specify the argument syntax -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. ``) indicate required command-arguments. -Square brackets (e.g. `[optional]`) indicate optional command-arguments. +You use `.argument` to specify the expected command-arguments. Angled brackets (e.g. ``) indicate a required command-argument. +Square brackets (e.g. `[optional]`) indicate an optional command-argument. The description parameter is optional. + +Example file: [argument.js](./examples/argument.js) ```js program @@ -439,23 +440,20 @@ program .argument('[password]', 'password for user, if required') .action((username, password) => { console.log('username:', username); - console.log('environment:', password || 'no password given'); + console.log('password:', password || 'no password given'); }); ``` -For more complex cases, use `.addArgument()` and pass `Argument` constructor. +There are two ways to specify all the arguments at once, but these do not allow argument descriptions. + ```js program - .version('0.1.0') - .addArgument(new Argument('', 'user to login')) - .action((username) => { - console.log('username:', username); - }); -``` - - -Example file: [argument.js](./examples/argument.js) + .command('add '); +program + .command('fraction') + .arguments(' '); +``` 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: @@ -471,7 +469,8 @@ program }); ``` -The variadic argument is passed to the action handler as an array. +A 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 From 0c43cd9cd4c1f727473a737a7e8f210aeea34e9b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 18:10:17 +1300 Subject: [PATCH 15/34] Add name method and improve Argument typings and tests --- index.js | 35 ++++++++++++++++++++++++----------- typings/index.d.ts | 20 ++++++++++++++------ typings/index.test-d.ts | 11 +++++++++++ 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 1e2eccb37..8e83a7190 100644 --- a/index.js +++ b/index.js @@ -90,8 +90,8 @@ class Help { 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] || ''; + const term = argument.name(); + const description = argument.description || legacyDescriptions[argument.name()] || ''; return { term, description }; }); }; @@ -359,28 +359,41 @@ class Argument { constructor(detail, description) { this.required = false; this.variadic = false; - this.name = ''; + this._name = ''; this.description = description || ''; switch (detail[0]) { case '<': // e.g. this.required = true; - this.name = detail.slice(1, -1); + this._name = detail.slice(1, -1); break; case '[': // e.g. [optional] - this.name = detail.slice(1, -1); + this._name = detail.slice(1, -1); break; } - if (this.name.length > 3 && this.name.slice(-3) === '...') { + if (this._name.length > 3 && this._name.slice(-3) === '...') { this.variadic = true; - this.name = this.name.slice(0, -3); + this._name = this._name.slice(0, -3); } - if (this.name.length === 0) { + if (this._name.length === 0) { throw new Error(`Unrecognised argument format (expecting '' or '[optional]'): ${detail}`); } } + + /** + * Get or set the name of the argument. + * + * @param {string} [str] + * @return {string|Command} + */ + + name(str) { + if (str === undefined) return this._name; + this._name = str; + return this; + }; } class Option { @@ -819,7 +832,7 @@ class Command extends EventEmitter { 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}'`); + throw new Error(`only the last argument can be variadic '${previousArgument.name()}'`); } this._args.push(argument); return this; @@ -1511,7 +1524,7 @@ class Command extends EventEmitter { const args = this.args.slice(); this._args.forEach((arg, i) => { if (arg.required && args[i] == null) { - this.missingArgument(arg.name); + this.missingArgument(arg.name()); } else if (arg.variadic) { args[i] = args.splice(i); args.length = Math.min(i + 1, args.length); @@ -2164,7 +2177,7 @@ function outputHelpIfRequested(cmd, args) { */ function humanReadableArgName(arg) { - const nameOutput = arg.name + (arg.variadic === true ? '...' : ''); + const nameOutput = arg.name() + (arg.variadic === true ? '...' : ''); return arg.required ? '<' + nameOutput + '>' diff --git a/typings/index.d.ts b/typings/index.d.ts index d79430204..6bced5296 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -22,12 +22,20 @@ declare namespace commander { interface Argument { description: string; - argDetails: { - required: boolean; - name: string; - variadic: boolean; - }; - } + required: boolean; + variadic: boolean; + + /** + * Set the name of the argument. + * + * @returns `this` command for chaining + */ + name(str: string): this; + /** + * Get the name of the argument. + */ + name(): string; + } interface Option { flags: string; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 07b1f5571..4faeb666c 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -331,3 +331,14 @@ expectType(baseOption.name()); // attributeName expectType(baseOption.attributeName()); + +// Argument properties +const baseArgument = new commander.Argument('(baseArgument.description); +expectType(baseArgument.required); +expectType(baseArgument.variadic); + +// Argument methods +// name +expectType(baseArgument.name()); +expectType(baseArgument.name('')); From 0dea8ac0d0fa20c844bcf751593a51478c7b731f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 18:14:19 +1300 Subject: [PATCH 16/34] Fix copy-and-paste JSDoc error --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 8e83a7190..c928f560e 100644 --- a/index.js +++ b/index.js @@ -386,7 +386,7 @@ class Argument { * Get or set the name of the argument. * * @param {string} [str] - * @return {string|Command} + * @return {string|Argument} */ name(str) { From 82ff5bd21bb426ef995fbb4dd011707bd60ae57c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 18:36:09 +1300 Subject: [PATCH 17/34] Update example to match new method and README --- examples/thank.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/thank.js b/examples/thank.js index acceb6b6b..c686c91a4 100644 --- a/examples/thank.js +++ b/examples/thank.js @@ -7,7 +7,7 @@ const { Command } = require('../'); // include commander in git clone of command const program = new Command(); program - .arguments('') + .argument('') .option('-t, --title ', 'title to use before name') .option('-d, --debug', 'display some debugging') .action((name, options, command) => { From a20a8febed1d0192073c4af622b4536b76d18292 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 27 Mar 2021 18:53:56 +1300 Subject: [PATCH 18/34] Deprecate old way of adding command argument descriptions --- docs/deprecated.md | 25 +++++++++++++++++++++++++ typings/index.d.ts | 5 ++++- typings/index.test-d.ts | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index b1c80fe71..d81d12073 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -86,3 +86,28 @@ Examples: ``` Deprecated from Commander v7. + +## cmd.description(cmdDescription, argDescriptions) + +This was used to add command argument descriptions for the help. + +```js +program + .command('price ') + .description('show price of book', { + book: 'ISBN number for book' + }); +``` + +The new approach is to use the `.argument()` method. + +```js +program + .command('price') + .description('show price of book') + .argument('', 'ISBN number for book'); +``` + + +Deprecated from Commander v8. + diff --git a/typings/index.d.ts b/typings/index.d.ts index 6bced5296..b050d7fe9 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -512,7 +512,10 @@ declare namespace commander { * * @returns `this` command for chaining */ - description(str: string, argsDescription?: {[argName: string]: string}): this; + + description(str: string): this; + /** @deprecated since v8, instead use .argument to add command argument with description */ + description(str: string, argsDescription: {[argName: string]: string}): this; /** * Get the description. */ diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 4faeb666c..192822c1a 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -178,6 +178,7 @@ expectType(opts['bar']); // description expectType(program.description('my description')); expectType(program.description()); +expectType(program.description('my description of command with arg foo', { foo: 'foo description'})); // deprecated // alias expectType(program.alias('my alias')); From df73a196b771d64aa764b2549f6996d73f78b41f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 12:35:31 +1300 Subject: [PATCH 19/34] Be lenient about Argument construction to allow lazy building --- index.js | 10 +++++----- tests/args.badArg.test.js | 7 ------- 2 files changed, 5 insertions(+), 12 deletions(-) delete mode 100644 tests/args.badArg.test.js diff --git a/index.js b/index.js index c928f560e..7e67adcfd 100644 --- a/index.js +++ b/index.js @@ -350,7 +350,7 @@ class Help { class Argument { /** - * Initialize a new argument with the given detail and description. + * Initialize a new command argument with the given detail and description. * * @param {string} detail * @param {string} [description] @@ -370,16 +370,16 @@ class Argument { case '[': // e.g. [optional] this._name = detail.slice(1, -1); break; + default: + this._name = detail; + this.required = true; + 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 '' or '[optional]'): ${detail}`); - } } /** diff --git a/tests/args.badArg.test.js b/tests/args.badArg.test.js deleted file mode 100644 index bbea378e4..000000000 --- a/tests/args.badArg.test.js +++ /dev/null @@ -1,7 +0,0 @@ -const commander = require('../'); - -test('when unexpected argument format then throw', () => { - const program = new commander.Command(); - expect(() => program.addArgument(new commander.Argument('bad-format'))).toThrow(); - expect(() => program.argument('bad-format')).toThrow(); -}); From 43ce351cd60e8bc1965a2715370de88f817fcc2b Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 17:20:08 +1300 Subject: [PATCH 20/34] Call first param to .argument "name", and expand jsdoc --- index.js | 37 ++++++++++++++++++++++--------------- typings/index.d.ts | 7 +++++-- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index 7e67adcfd..d2dd41e7d 100644 --- a/index.js +++ b/index.js @@ -350,28 +350,30 @@ class Help { class Argument { /** - * Initialize a new command argument with the given detail and description. + * Initialize a new command argument with the given name and description. + * The default is that the argument is required, and you can explicitly + * indicate this with <> around the name. Put [] around the name for an optional argument. * - * @param {string} detail + * @param {string} name * @param {string} [description] */ - constructor(detail, description) { + constructor(name, description) { this.required = false; this.variadic = false; this._name = ''; this.description = description || ''; - switch (detail[0]) { + switch (name[0]) { case '<': // e.g. this.required = true; - this._name = detail.slice(1, -1); + this._name = name.slice(1, -1); break; case '[': // e.g. [optional] - this._name = detail.slice(1, -1); + this._name = name.slice(1, -1); break; default: - this._name = detail; + this._name = name; this.required = true; break; } @@ -818,8 +820,8 @@ class Command extends EventEmitter { * Define argument syntax for the command. */ - arguments(details) { - details.split(/ +/).forEach((detail) => { + arguments(names) { + names.split(/ +/).forEach((detail) => { this.argument(detail); }); return this; @@ -827,6 +829,7 @@ class Command extends EventEmitter { /** * Define argument syntax for the command. + * * @param {Argument} argument */ addArgument(argument) { @@ -839,12 +842,16 @@ class Command extends EventEmitter { } /** - * Define argument syntax for the command - * @param {string} detail - * @param {string} [description] - */ - argument(detail, description) { - const argument = new Argument(detail, description); + * Define argument syntax for the command + * + * The default is that the argument is required, and you can explicitly + * indicate this with <> around the name. Put [] around the name for an optional argument. + * + * @param {string} name + * @param {string} [description] + */ + argument(name, description) { + const argument = new Argument(name, description); this.addArgument(argument); return this; } diff --git a/typings/index.d.ts b/typings/index.d.ts index b050d7fe9..7f2ab1141 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -254,11 +254,14 @@ declare namespace commander { /** * Define argument syntax for command. * + * The default is that the argument is required, and you can explicitly + * indicate this with <> around the name. Put [] around the name for an optional argument. + * * @returns `this` command for chaining */ - arguments(desc: string): this; - argument(arg: string, description?: string): this; + argument(name: string, description?: string): this; addArgument(arg: Argument): this; + arguments(names: string): this; /** * Override default decision whether to add implicit help command. From af08a2b4ecd0f90aa0994a81b75269c8c24eadff Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 17:57:46 +1300 Subject: [PATCH 21/34] Add low-level check that get same Argument from multiple ways of specifying argument --- tests/command.argumentVariations.test.js | 79 ++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/command.argumentVariations.test.js diff --git a/tests/command.argumentVariations.test.js b/tests/command.argumentVariations.test.js new file mode 100644 index 000000000..f38a8dafc --- /dev/null +++ b/tests/command.argumentVariations.test.js @@ -0,0 +1,79 @@ +const commander = require('../'); + +// Do some low-level checks that the multiple ways of specifying command arguments produce same internal result, +// and not exhaustively testing all methods elsewhere. + +test.each(getTestCases(''))('when add "" via %s then argument required', (methodName, cmd) => { + const argument = cmd._args[0]; + const expectedShape = { + _name: 'explicit-required', + required: true, + variadic: false, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +test.each(getTestCases('implicit-required'))('when add "arg" via %s then argument required', (methodName, cmd) => { + const argument = cmd._args[0]; + const expectedShape = { + _name: 'implicit-required', + required: true, + variadic: false, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +test.each(getTestCases('[optional]'))('when add "[arg]" via %s then argument optional', (methodName, cmd) => { + const argument = cmd._args[0]; + const expectedShape = { + _name: 'optional', + required: false, + variadic: false, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +test.each(getTestCases(''))('when add "" via %s then argument required and variadic', (methodName, cmd) => { + const argument = cmd._args[0]; + const expectedShape = { + _name: 'explicit-required', + required: true, + variadic: true, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +test.each(getTestCases('implicit-required...'))('when add "arg..." via %s then argument required and variadic', (methodName, cmd) => { + const argument = cmd._args[0]; + const expectedShape = { + _name: 'implicit-required', + required: true, + variadic: true, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +test.each(getTestCases('[optional...]'))('when add "[arg...]" via %s then argument optional and variadic', (methodName, cmd) => { + const argument = cmd._args[0]; + const expectedShape = { + _name: 'optional', + required: false, + variadic: true, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +function getTestCases(arg) { + return [ + ['.arguments', new commander.Command().arguments(arg)], + ['.argument', new commander.Command().argument(arg)], + ['.addArgument', new commander.Command('add-argument').addArgument(new commander.Argument(arg))], + ['.command', new commander.Command().command(`command ${arg}`)] + ]; +} From bac5ea9e606b3d27c7125281ba8b076b9dd4e7a8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 18:07:02 +1300 Subject: [PATCH 22/34] Minor wording tweaks --- Readme.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 41eeee1cf..9331c8ee4 100644 --- a/Readme.md +++ b/Readme.md @@ -428,8 +428,9 @@ subcommand is specified ([example](./examples/defaultCommand.js)). ### Specify the argument syntax -You use `.argument` to specify the expected command-arguments. Angled brackets (e.g. ``) indicate a required command-argument. -Square brackets (e.g. `[optional]`) indicate an optional command-argument. The description parameter is optional. +You use `.argument` to specify the expected command-arguments. +Angle brackets around the name indicate a required command-argument (e.g. ``). +Square brackets indicate an optional command-argument (e.g. `[optional]`). The description parameter is optional. Example file: [argument.js](./examples/argument.js) From 0e505be606dcfba6fe03d4d9908c2ba8030adf95 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 18:35:33 +1300 Subject: [PATCH 23/34] Add low-level tests for multiple arg variations --- tests/command.argumentVariations.test.js | 39 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/tests/command.argumentVariations.test.js b/tests/command.argumentVariations.test.js index f38a8dafc..a8a781625 100644 --- a/tests/command.argumentVariations.test.js +++ b/tests/command.argumentVariations.test.js @@ -3,7 +3,7 @@ const commander = require('../'); // Do some low-level checks that the multiple ways of specifying command arguments produce same internal result, // and not exhaustively testing all methods elsewhere. -test.each(getTestCases(''))('when add "" via %s then argument required', (methodName, cmd) => { +test.each(getSingleArgCases(''))('when add "" using %s then argument required', (methodName, cmd) => { const argument = cmd._args[0]; const expectedShape = { _name: 'explicit-required', @@ -14,7 +14,7 @@ test.each(getTestCases(''))('when add "" via %s then arg expect(argument).toEqual(expectedShape); }); -test.each(getTestCases('implicit-required'))('when add "arg" via %s then argument required', (methodName, cmd) => { +test.each(getSingleArgCases('implicit-required'))('when add "arg" using %s then argument required', (methodName, cmd) => { const argument = cmd._args[0]; const expectedShape = { _name: 'implicit-required', @@ -25,7 +25,7 @@ test.each(getTestCases('implicit-required'))('when add "arg" via %s then argumen expect(argument).toEqual(expectedShape); }); -test.each(getTestCases('[optional]'))('when add "[arg]" via %s then argument optional', (methodName, cmd) => { +test.each(getSingleArgCases('[optional]'))('when add "[arg]" using %s then argument optional', (methodName, cmd) => { const argument = cmd._args[0]; const expectedShape = { _name: 'optional', @@ -36,7 +36,7 @@ test.each(getTestCases('[optional]'))('when add "[arg]" via %s then argument opt expect(argument).toEqual(expectedShape); }); -test.each(getTestCases(''))('when add "" via %s then argument required and variadic', (methodName, cmd) => { +test.each(getSingleArgCases(''))('when add "" using %s then argument required and variadic', (methodName, cmd) => { const argument = cmd._args[0]; const expectedShape = { _name: 'explicit-required', @@ -47,7 +47,7 @@ test.each(getTestCases(''))('when add "" via %s th expect(argument).toEqual(expectedShape); }); -test.each(getTestCases('implicit-required...'))('when add "arg..." via %s then argument required and variadic', (methodName, cmd) => { +test.each(getSingleArgCases('implicit-required...'))('when add "arg..." using %s then argument required and variadic', (methodName, cmd) => { const argument = cmd._args[0]; const expectedShape = { _name: 'implicit-required', @@ -58,7 +58,7 @@ test.each(getTestCases('implicit-required...'))('when add "arg..." via %s then a expect(argument).toEqual(expectedShape); }); -test.each(getTestCases('[optional...]'))('when add "[arg...]" via %s then argument optional and variadic', (methodName, cmd) => { +test.each(getSingleArgCases('[optional...]'))('when add "[arg...]" using %s then argument optional and variadic', (methodName, cmd) => { const argument = cmd._args[0]; const expectedShape = { _name: 'optional', @@ -69,7 +69,7 @@ test.each(getTestCases('[optional...]'))('when add "[arg...]" via %s then argume expect(argument).toEqual(expectedShape); }); -function getTestCases(arg) { +function getSingleArgCases(arg) { return [ ['.arguments', new commander.Command().arguments(arg)], ['.argument', new commander.Command().argument(arg)], @@ -77,3 +77,28 @@ function getTestCases(arg) { ['.command', new commander.Command().command(`command ${arg}`)] ]; } + +test.each(getMultipleArgCases('', '[second]'))('when add two arguments using %s then two arguments', (methodName, cmd) => { + expect(cmd._args[0].name()).toEqual('first'); + expect(cmd._args[1].name()).toEqual('second'); +}); + +function getMultipleArgCases(arg1, arg2) { + return [ + ['.arguments', new commander.Command().arguments(`${arg1} ${arg2}`)], + ['.argument', new commander.Command().argument(arg1).argument(arg2)], + ['.addArgument', new commander.Command('add-argument').addArgument(new commander.Argument(arg1)).addArgument(new commander.Argument(arg2))], + ['.command', new commander.Command().command(`command ${arg1} ${arg2}`)] + ]; +} + +test('when add arguments using multiple methods then all added', () => { + // This is not a key use case, but explicitly test that additive behaviour. + const program = new commander.Command(); + const cmd = program.command('sub '); + cmd.arguments(' '); + cmd.argument(''); + cmd.addArgument(new commander.Argument('arg6')); + const argNames = cmd._args.map(arg => arg.name()); + expect(argNames).toEqual(['arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6']); +}); From dac96562cf4f2ceda1e5f393aaad46c414079f72 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 19:25:51 +1300 Subject: [PATCH 24/34] Simplify test. Use .argument now. --- tests/command.usage.test.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/command.usage.test.js b/tests/command.usage.test.js index b3088fe8d..eee4d9bfc 100644 --- a/tests/command.usage.test.js +++ b/tests/command.usage.test.js @@ -78,22 +78,20 @@ test('when no commands then [command] not included in usage', () => { expect(program.usage()).not.toMatch('[command]'); }); -test('when arguments then arguments included in usage', () => { +test('when argument then argument included in usage', () => { const program = new commander.Command(); program - .arguments('') - .argument('') - .addArgument(new commander.Argument('')); + .argument(''); - expect(program.usage()).toMatch(' '); + expect(program.usage()).toMatch(''); }); -test('when options and command and arguments then all three included in usage', () => { +test('when options and command and argument then all three included in usage', () => { const program = new commander.Command(); program - .arguments('') + .argument('') .option('--alpha') .command('beta'); From 374565934c2c18b74c24f95f8446bfe746a09761 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 19:32:12 +1300 Subject: [PATCH 25/34] Restore simple test, use .argument --- tests/help.commandUsage.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/help.commandUsage.test.js b/tests/help.commandUsage.test.js index 941b0f9b3..08cd776bd 100644 --- a/tests/help.commandUsage.test.js +++ b/tests/help.commandUsage.test.js @@ -42,10 +42,8 @@ describe('commandUsage', () => { const program = new commander.Command(); program .name('program') - .arguments('') - .argument('', 'description') - .addArgument(new commander.Argument('', 'info')); + .argument(''); const helper = new commander.Help(); - expect(helper.commandUsage(program)).toEqual('program [options] '); + expect(helper.commandUsage(program)).toEqual('program [options] '); }); }); From a76f831ee06ccb8e7389edb98963ce5ef1413cf5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 20:13:09 +1300 Subject: [PATCH 26/34] Big switch from .arguments to .argument in tests --- docs/options-taking-varying-arguments.md | 4 +-- ...60\347\232\204\351\200\211\351\241\271.md" | 4 +-- examples/pass-through-options.js | 3 ++- tests/command.allowExcessArguments.test.js | 8 +++--- tests/command.allowUnknownOption.test.js | 2 +- tests/command.asterisk.test.js | 8 +++--- tests/command.chain.test.js | 14 +++++++++- tests/command.help.test.js | 17 +++++++++--- tests/command.positionalOptions.test.js | 26 +++++++++---------- tests/command.unknownCommand.test.js | 2 +- tests/command.unknownOption.test.js | 4 +-- tests/commander.configureCommand.test.js | 6 ++--- tests/help.commandTerm.test.js | 4 +-- tests/help.longestArgumentTermLength.test.js | 12 +++------ tests/help.padWidth.test.js | 9 +++---- tests/help.visibleArguments.test.js | 5 ++-- tests/options.variadic.test.js | 6 ++--- 17 files changed, 74 insertions(+), 60 deletions(-) diff --git a/docs/options-taking-varying-arguments.md b/docs/options-taking-varying-arguments.md index 560487a86..58a52956e 100644 --- a/docs/options-taking-varying-arguments.md +++ b/docs/options-taking-varying-arguments.md @@ -5,7 +5,7 @@ and subtle issues in depth. - [Options taking varying numbers of option-arguments](#options-taking-varying-numbers-of-option-arguments) - [Parsing ambiguity](#parsing-ambiguity) - - [Alternative: Make `--` part of your syntax](#alternative-make----part-of-your-syntax) + - [Alternative: Make `--` part of your syntax](#alternative-make-----part-of-your-syntax) - [Alternative: Put options last](#alternative-put-options-last) - [Alternative: Use options instead of command-arguments](#alternative-use-options-instead-of-command-arguments) - [Combining short options, and options taking arguments](#combining-short-options-and-options-taking-arguments) @@ -34,7 +34,7 @@ intend the argument following the option as a command or command-argument. ```js program .name('cook') - .arguments('[technique]') + .argument('[technique]') .option('-i, --ingredient [ingredient]', 'add cheese or given ingredient') .action((technique, options) => { console.log(`technique: ${technique}`); diff --git "a/docs/zh-CN/\345\217\257\345\217\230\345\217\202\346\225\260\347\232\204\351\200\211\351\241\271.md" "b/docs/zh-CN/\345\217\257\345\217\230\345\217\202\346\225\260\347\232\204\351\200\211\351\241\271.md" index 77f93035b..9aeb37682 100644 --- "a/docs/zh-CN/\345\217\257\345\217\230\345\217\202\346\225\260\347\232\204\351\200\211\351\241\271.md" +++ "b/docs/zh-CN/\345\217\257\345\217\230\345\217\202\346\225\260\347\232\204\351\200\211\351\241\271.md" @@ -31,7 +31,7 @@ Commander 首先解析选项的参数,而用户有可能想将选项后面跟 ```js program .name('cook') - .arguments('[technique]') + .argument('[technique]') .option('-i, --ingredient [ingredient]', 'add cheese or given ingredient') .action((technique, options) => { console.log(`technique: ${technique}`); @@ -190,4 +190,4 @@ halal servings: true ```js .combineFlagAndOptionalValue(true) // `-v45` 被视为 `--vegan=45`,这是默认的行为 .combineFlagAndOptionalValue(false) // `-vl` 被视为 `-v -l` -``` \ No newline at end of file +``` diff --git a/examples/pass-through-options.js b/examples/pass-through-options.js index af5c8497c..88ee40374 100644 --- a/examples/pass-through-options.js +++ b/examples/pass-through-options.js @@ -5,7 +5,8 @@ const { Command } = require('../'); // include commander in git clone of command const program = new Command(); program - .arguments(' [args...]') + .argument('') + .argument('[args...]') .passThroughOptions() .option('-d, --dry-run') .action((utility, args, options) => { diff --git a/tests/command.allowExcessArguments.test.js b/tests/command.allowExcessArguments.test.js index 9431a1888..99fbaf212 100644 --- a/tests/command.allowExcessArguments.test.js +++ b/tests/command.allowExcessArguments.test.js @@ -94,7 +94,7 @@ describe('allowUnknownOption', () => { test('when specify expected arg and allowExcessArguments(false) then no error', () => { const program = new commander.Command(); program - .arguments('') + .argument('') .exitOverride() .allowExcessArguments(false) .action(() => {}); @@ -107,7 +107,7 @@ describe('allowUnknownOption', () => { test('when specify excess after and allowExcessArguments(false) then error', () => { const program = new commander.Command(); program - .arguments('') + .argument('') .exitOverride() .allowExcessArguments(false) .action(() => {}); @@ -120,7 +120,7 @@ describe('allowUnknownOption', () => { test('when specify excess after [arg] and allowExcessArguments(false) then error', () => { const program = new commander.Command(); program - .arguments('[file]') + .argument('[file]') .exitOverride() .allowExcessArguments(false) .action(() => {}); @@ -133,7 +133,7 @@ describe('allowUnknownOption', () => { test('when specify args for [args...] and allowExcessArguments(false) then no error', () => { const program = new commander.Command(); program - .arguments('[files...]') + .argument('[files...]') .exitOverride() .allowExcessArguments(false) .action(() => {}); diff --git a/tests/command.allowUnknownOption.test.js b/tests/command.allowUnknownOption.test.js index 5459c5154..6bdd72eed 100644 --- a/tests/command.allowUnknownOption.test.js +++ b/tests/command.allowUnknownOption.test.js @@ -83,7 +83,7 @@ describe('allowUnknownOption', () => { program .exitOverride() .command('sub') - .arguments('[args...]') // unknown option will be passed as an argument + .argument('[args...]') // unknown option will be passed as an argument .allowUnknownOption() .option('-p, --pepper', 'add pepper') .action(() => { }); diff --git a/tests/command.asterisk.test.js b/tests/command.asterisk.test.js index d975c868b..b27e0c7cc 100644 --- a/tests/command.asterisk.test.js +++ b/tests/command.asterisk.test.js @@ -33,7 +33,7 @@ describe(".command('*')", () => { const program = new commander.Command(); program .command('*') - .arguments('[args...]') + .argument('[args...]') .action(mockAction); program.parse(['node', 'test', 'unrecognised-command']); expect(mockAction).toHaveBeenCalled(); @@ -59,7 +59,7 @@ describe(".command('*')", () => { .command('install'); program .command('*') - .arguments('[args...]') + .argument('[args...]') .action(mockAction); program.parse(['node', 'test', 'unrecognised-command']); expect(mockAction).toHaveBeenCalled(); @@ -73,7 +73,7 @@ describe(".command('*')", () => { .command('install'); const star = program .command('*') - .arguments('[args...]') + .argument('[args...]') .option('-d, --debug') .action(mockAction); program.parse(['node', 'test', 'unrecognised-command', '--debug']); @@ -93,7 +93,7 @@ describe(".command('*')", () => { .command('install'); program .command('*') - .arguments('[args...]') + .argument('[args...]') .action(mockAction); let caughtErr; try { diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 4cadbd074..79c08a815 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -1,4 +1,4 @@ -const { Command, Option } = require('../'); +const { Command, Option, Argument } = require('../'); // Testing the functions which should chain. // parse and parseAsync are tested in command.parse.test.js @@ -16,6 +16,18 @@ describe('Command methods that should return this for chaining', () => { expect(result).toBe(program); }); + test('when call .argument() then returns this', () => { + const program = new Command(); + const result = program.argument(''); + expect(result).toBe(program); + }); + + test('when call .addArgument() then returns this', () => { + const program = new Command(); + const result = program.addArgument(new Argument('')); + expect(result).toBe(program); + }); + test('when set .arguments() then returns this', () => { const program = new Command(); const result = program.arguments(''); diff --git a/tests/command.help.test.js b/tests/command.help.test.js index 43e0fb01b..ec1ba4172 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -235,16 +235,25 @@ test('when option has choices and default then both included in helpInformation' expect(helpInformation).toMatch('(choices: "red", "blue", default: "red")'); }); -test('when arguments then included in helpInformation', () => { +test('when argument then included in helpInformation', () => { const program = new commander.Command(); program .name('foo') - .arguments(''); + .argument(''); const helpInformation = program.helpInformation(); expect(helpInformation).toMatch('Usage: foo [options] '); }); -test('when arguments described then included in helpInformation', () => { +test('when argument described then included in helpInformation', () => { + const program = new commander.Command(); + program + .argument('', 'input source') + .helpOption(false); + const helpInformation = program.helpInformation(); + expect(helpInformation).toMatch(/Arguments:\n +file +input source/); +}); + +test('when arguments described in deprecated way then included in helpInformation', () => { const program = new commander.Command(); program .arguments('') @@ -254,7 +263,7 @@ test('when arguments described then included in helpInformation', () => { expect(helpInformation).toMatch(/Arguments:\n +file +input source/); }); -test('when arguments described and empty description then arguments included in helpInformation', () => { +test('when arguments described in deprecated way and empty description then arguments still included in helpInformation', () => { const program = new commander.Command(); program .arguments('') diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js index ab3c37ec0..d130b7835 100644 --- a/tests/command.positionalOptions.test.js +++ b/tests/command.positionalOptions.test.js @@ -9,7 +9,7 @@ describe('program with passThrough', () => { program.passThroughOptions(); program .option('-d, --debug') - .arguments(''); + .argument(''); return program; } @@ -77,10 +77,10 @@ describe('program with positionalOptions and subcommand', () => { program .enablePositionalOptions() .option('-s, --shared ') - .arguments(''); + .argument(''); const sub = program .command('sub') - .arguments('[arg]') + .argument('[arg]') .option('-s, --shared ') .action(() => {}); // Not used, but normal to have action handler on subcommand. return { program, sub }; @@ -151,10 +151,10 @@ describe('program with positionalOptions and default subcommand (called sub)', ( .enablePositionalOptions() .option('-s, --shared') .option('-g, --global') - .arguments(''); + .argument(''); const sub = program .command('sub', { isDefault: true }) - .arguments('[args...]') + .argument('[args...]') .option('-s, --shared') .option('-d, --default') .action(() => {}); // Not used, but normal to have action handler on subcommand. @@ -218,11 +218,11 @@ describe('subcommand with passThrough', () => { program .enablePositionalOptions() .option('-s, --shared ') - .arguments(''); + .argument(''); const sub = program .command('sub') .passThroughOptions() - .arguments('[args...]') + .argument('[args...]') .option('-s, --shared ') .option('-d, --debug') .action(() => {}); // Not used, but normal to have action handler on subcommand. @@ -289,7 +289,7 @@ describe('default command with passThrough', () => { const sub = program .command('sub', { isDefault: true }) .passThroughOptions() - .arguments('[args...]') + .argument('[args...]') .option('-d, --debug') .action(() => {}); // Not used, but normal to have action handler on subcommand. return { program, sub }; @@ -332,11 +332,11 @@ describe('program with action handler and positionalOptions and subcommand', () program .enablePositionalOptions() .option('-g, --global') - .arguments('') + .argument('') .action(() => {}); const sub = program .command('sub') - .arguments('[arg]') + .argument('[arg]') .action(() => {}); return { program, sub }; } @@ -379,11 +379,11 @@ describe('program with action handler and passThrough and subcommand', () => { program .passThroughOptions() .option('-g, --global') - .arguments('') + .argument('') .action(() => {}); const sub = program .command('sub') - .arguments('[arg]') + .argument('[arg]') .option('-g, --group') .option('-d, --debug') .action(() => {}); @@ -451,7 +451,7 @@ describe('passThroughOptions(xxx) and option after command-argument', () => { const program = new commander.Command(); program .option('-d, --debug') - .arguments(''); + .argument(''); return program; } diff --git a/tests/command.unknownCommand.test.js b/tests/command.unknownCommand.test.js index 7e8bfa0d9..937bc54e0 100644 --- a/tests/command.unknownCommand.test.js +++ b/tests/command.unknownCommand.test.js @@ -30,7 +30,7 @@ describe('unknownCommand', () => { .exitOverride() .command('sub'); program - .arguments('[args...]') + .argument('[args...]') .action(() => { }); expect(() => { program.parse('node test.js unknown'.split(' ')); diff --git a/tests/command.unknownOption.test.js b/tests/command.unknownOption.test.js index 4bceeeb8f..7ab72237d 100644 --- a/tests/command.unknownOption.test.js +++ b/tests/command.unknownOption.test.js @@ -54,7 +54,7 @@ describe('unknownOption', () => { const program = new commander.Command(); program .exitOverride() - .arguments('[file]') + .argument('[file]') .action(() => {}); let caughtErr; @@ -71,7 +71,7 @@ describe('unknownOption', () => { const program = new commander.Command(); program .exitOverride() - .arguments('[file]') + .argument('[file]') .action(() => {}); let caughtErr; diff --git a/tests/commander.configureCommand.test.js b/tests/commander.configureCommand.test.js index 5388a1866..b15dfb97c 100644 --- a/tests/commander.configureCommand.test.js +++ b/tests/commander.configureCommand.test.js @@ -17,7 +17,7 @@ test('when default then options+command passed to action', () => { const program = new commander.Command(); const callback = jest.fn(); program - .arguments('') + .argument('') .action(callback); program.parse(['node', 'test', 'value']); expect(callback).toHaveBeenCalledWith('value', program.opts(), program); @@ -60,7 +60,7 @@ test('when storeOptionsAsProperties() then command+command passed to action', () const callback = jest.fn(); program .storeOptionsAsProperties() - .arguments('') + .argument('') .action(callback); program.parse(['node', 'test', 'value']); expect(callback).toHaveBeenCalledWith('value', program, program); @@ -71,7 +71,7 @@ test('when storeOptionsAsProperties(false) then opts+command passed to action', const callback = jest.fn(); program .storeOptionsAsProperties(false) - .arguments('') + .argument('') .action(callback); program.parse(['node', 'test', 'value']); expect(callback).toHaveBeenCalledWith('value', program.opts(), program); diff --git a/tests/help.commandTerm.test.js b/tests/help.commandTerm.test.js index cfb003a3e..a2e1c820c 100644 --- a/tests/help.commandTerm.test.js +++ b/tests/help.commandTerm.test.js @@ -27,7 +27,7 @@ describe('subcommandTerm', () => { test('when command has then returns name ', () => { const command = new commander.Command('program') - .arguments(''); + .argument(''); const helper = new commander.Help(); expect(helper.subcommandTerm(command)).toEqual('program '); }); @@ -36,7 +36,7 @@ describe('subcommandTerm', () => { const command = new commander.Command('program') .alias('alias') .option('-a,--all') - .arguments(''); + .argument(''); const helper = new commander.Help(); expect(helper.subcommandTerm(command)).toEqual('program|alias [options] '); }); diff --git a/tests/help.longestArgumentTermLength.test.js b/tests/help.longestArgumentTermLength.test.js index 4a0340d39..5995d0965 100644 --- a/tests/help.longestArgumentTermLength.test.js +++ b/tests/help.longestArgumentTermLength.test.js @@ -12,20 +12,16 @@ describe('longestArgumentTermLength', () => { test('when has argument description then returns argument length', () => { const program = new commander.Command(); - program.arguments(''); - program.description('dummy', { wonder: 'wonder description' }); + program.argument('', 'wonder description'); const helper = new commander.Help(); expect(helper.longestArgumentTermLength(program, helper)).toEqual('wonder'.length); }); test('when has multiple argument descriptions then returns longest', () => { const program = new commander.Command(); - program.arguments(' '); - program.description('dummy', { - alpha: 'x', - longest: 'x', - beta: 'x' - }); + program.argument('', 'x'); + program.argument('', 'x'); + program.argument('', 'x'); const helper = new commander.Help(); expect(helper.longestArgumentTermLength(program, helper)).toEqual('longest'.length); }); diff --git a/tests/help.padWidth.test.js b/tests/help.padWidth.test.js index 496e5b2e0..affa3e201 100644 --- a/tests/help.padWidth.test.js +++ b/tests/help.padWidth.test.js @@ -8,8 +8,7 @@ describe('padWidth', () => { const longestThing = 'veryLongThingBiggerThanOthers'; const program = new commander.Command(); program - .arguments(`<${longestThing}>`) - .description('description', { veryLongThingBiggerThanOthers: 'desc' }) + .argument(`<${longestThing}>`, 'description') .option('-o'); program .command('sub'); @@ -21,8 +20,7 @@ describe('padWidth', () => { const longestThing = '--very-long-thing-bigger-than-others'; const program = new commander.Command(); program - .arguments('') - .description('description', { file: 'desc' }) + .argument('', 'desc') .option(longestThing); program .command('sub'); @@ -34,8 +32,7 @@ describe('padWidth', () => { const longestThing = 'very-long-thing-bigger-than-others'; const program = new commander.Command(); program - .arguments('') - .description('description', { file: 'desc' }) + .argument('', 'desc') .option('-o'); program .command(longestThing); diff --git a/tests/help.visibleArguments.test.js b/tests/help.visibleArguments.test.js index 98d884e18..ac80aa7fe 100644 --- a/tests/help.visibleArguments.test.js +++ b/tests/help.visibleArguments.test.js @@ -12,15 +12,14 @@ describe('visibleArguments', () => { test('when argument but no argument description then empty array', () => { const program = new commander.Command(); - program.arguments(''); + program.argument(''); const helper = new commander.Help(); expect(helper.visibleArguments(program)).toEqual([]); }); test('when argument and argument description then returned', () => { const program = new commander.Command(); - program.arguments(''); - program.description('dummy', { file: 'file description' }); + program.argument('', 'file description'); const helper = new commander.Help(); expect(helper.visibleArguments(program)).toEqual([{ term: 'file', description: 'file description' }]); }); diff --git a/tests/options.variadic.test.js b/tests/options.variadic.test.js index 870797448..346afe0a0 100644 --- a/tests/options.variadic.test.js +++ b/tests/options.variadic.test.js @@ -62,7 +62,7 @@ describe('variadic option with required value', () => { const program = new commander.Command(); program .option('-r,--required ') - .arguments('[arg]'); + .argument('[arg]'); program.parse(['-rone', 'operand'], { from: 'user' }); expect(program.opts().required).toEqual(['one']); @@ -72,7 +72,7 @@ describe('variadic option with required value', () => { const program = new commander.Command(); program .option('-r,--required ') - .arguments('[arg]'); + .argument('[arg]'); program.parse(['--required=one', 'operand'], { from: 'user' }); expect(program.opts().required).toEqual(['one']); @@ -83,7 +83,7 @@ describe('variadic option with required value', () => { program .option('-r,--required ') .option('-f, --flag') - .arguments('[arg]'); + .argument('[arg]'); program.parse(['-r', 'one', '-f'], { from: 'user' }); const opts = program.opts(); From 5a0857133ec0358e5d71320c794ed5b93fd2c478 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 28 Mar 2021 20:39:02 +1300 Subject: [PATCH 27/34] Expand help to explain argument variations --- index.js | 50 ++++++++++++++++++++++++++++++---------------- typings/index.d.ts | 23 +++++++++++++++++++++ 2 files changed, 56 insertions(+), 17 deletions(-) diff --git a/index.js b/index.js index d2dd41e7d..2f3b1dd14 100644 --- a/index.js +++ b/index.js @@ -817,7 +817,37 @@ class Command extends EventEmitter { }; /** - * Define argument syntax for the command. + * Define argument syntax for command. + * + * The default is that the argument is required, and you can explicitly + * indicate this with <> around the name. Put [] around the name for an optional argument. + * + * @example + * + * program.argument(''); + * program.argument('[output-file]'); + * + * @param {string} name + * @param {string} [description] + * @return {Command} `this` command for chaining + */ + argument(name, description) { + const argument = new Argument(name, description); + this.addArgument(argument); + return this; + } + + /** + * Define argument syntax for command, adding multiple at once (without descriptions). + * + * See also .argument(). + * + * @example + * + * program.arguments(' [env]'); + * + * @param {string} names + * @return {Command} `this` command for chaining */ arguments(names) { @@ -828,9 +858,10 @@ class Command extends EventEmitter { }; /** - * Define argument syntax for the command. + * Define argument syntax for command, adding a prepared argument. * * @param {Argument} argument + * @return {Command} `this` command for chaining */ addArgument(argument) { const previousArgument = this._args.slice(-1)[0]; @@ -841,21 +872,6 @@ class Command extends EventEmitter { return this; } - /** - * Define argument syntax for the command - * - * The default is that the argument is required, and you can explicitly - * indicate this with <> around the name. Put [] around the name for an optional argument. - * - * @param {string} name - * @param {string} [description] - */ - argument(name, description) { - const argument = new Argument(name, description); - this.addArgument(argument); - return this; - } - /** * Override default decision whether to add implicit help command. * diff --git a/typings/index.d.ts b/typings/index.d.ts index 7f2ab1141..be892dade 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -257,10 +257,33 @@ declare namespace commander { * The default is that the argument is required, and you can explicitly * indicate this with <> around the name. Put [] around the name for an optional argument. * + * @example + * + * program.argument(''); + * program.argument('[output-file]'); + * * @returns `this` command for chaining */ argument(name: string, description?: string): this; + + /** + * Define argument syntax for command, adding a prepared argument. + * + * @returns `this` command for chaining + */ addArgument(arg: Argument): this; + + /** + * Define argument syntax for command, adding multiple at once (without descriptions). + * + * See also .argument(). + * + * @example + * + * program.arguments(' [env]'); + * + * @returns `this` command for chaining + */ arguments(names: string): this; /** From 747d6ef1023688d2d9c7c42e69d7bd430719c784 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 29 Mar 2021 22:45:25 +1300 Subject: [PATCH 28/34] Keep Argument properties private for now (like Command, unlike Option) --- index.js | 28 +++++++++--------- tests/command.argumentVariations.test.js | 36 ++++++++++++------------ typings/index.d.ts | 6 ++-- typings/index.test-d.ts | 6 ++-- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/index.js b/index.js index 2f3b1dd14..aaf754fbb 100644 --- a/index.js +++ b/index.js @@ -87,11 +87,11 @@ class Help { visibleArguments(cmd) { // If there are some argument description then return all the arguments. - if (cmd._argsDescription || cmd._args.find(argument => argument.description)) { + 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()] || ''; + const description = argument._description || legacyDescriptions[argument.name()] || ''; return { term, description }; }); }; @@ -359,14 +359,14 @@ class Argument { */ constructor(name, description) { - this.required = false; - this.variadic = false; + this._required = false; + this._variadic = false; this._name = ''; - this.description = description || ''; + this._description = description || ''; switch (name[0]) { case '<': // e.g. - this.required = true; + this._required = true; this._name = name.slice(1, -1); break; case '[': // e.g. [optional] @@ -374,12 +374,12 @@ class Argument { break; default: this._name = name; - this.required = true; + this._required = true; break; } if (this._name.length > 3 && this._name.slice(-3) === '...') { - this.variadic = true; + this._variadic = true; this._name = this._name.slice(0, -3); } } @@ -865,7 +865,7 @@ class Command extends EventEmitter { */ addArgument(argument) { const previousArgument = this._args.slice(-1)[0]; - if (previousArgument && previousArgument.variadic) { + if (previousArgument && previousArgument._variadic) { throw new Error(`only the last argument can be variadic '${previousArgument.name()}'`); } this._args.push(argument); @@ -1546,9 +1546,9 @@ class Command extends EventEmitter { // Check expected arguments and collect variadic together. const args = this.args.slice(); this._args.forEach((arg, i) => { - if (arg.required && args[i] == null) { + if (arg._required && args[i] == null) { this.missingArgument(arg.name()); - } else if (arg.variadic) { + } else if (arg._variadic) { args[i] = args.splice(i); args.length = Math.min(i + 1, args.length); } @@ -2194,15 +2194,15 @@ function outputHelpIfRequested(cmd, args) { /** * Takes an argument and returns its human readable equivalent for help usage. * - * @param {Object} arg + * @param {Argument} arg * @return {string} * @api private */ function humanReadableArgName(arg) { - const nameOutput = arg.name() + (arg.variadic === true ? '...' : ''); + const nameOutput = arg.name() + (arg._variadic === true ? '...' : ''); - return arg.required + return arg._required ? '<' + nameOutput + '>' : '[' + nameOutput + ']'; } diff --git a/tests/command.argumentVariations.test.js b/tests/command.argumentVariations.test.js index a8a781625..2ea805a59 100644 --- a/tests/command.argumentVariations.test.js +++ b/tests/command.argumentVariations.test.js @@ -7,9 +7,9 @@ test.each(getSingleArgCases(''))('when add "" using %s t const argument = cmd._args[0]; const expectedShape = { _name: 'explicit-required', - required: true, - variadic: false, - description: '' + _required: true, + _variadic: false, + _description: '' }; expect(argument).toEqual(expectedShape); }); @@ -18,9 +18,9 @@ test.each(getSingleArgCases('implicit-required'))('when add "arg" using %s then const argument = cmd._args[0]; const expectedShape = { _name: 'implicit-required', - required: true, - variadic: false, - description: '' + _required: true, + _variadic: false, + _description: '' }; expect(argument).toEqual(expectedShape); }); @@ -29,9 +29,9 @@ test.each(getSingleArgCases('[optional]'))('when add "[arg]" using %s then argum const argument = cmd._args[0]; const expectedShape = { _name: 'optional', - required: false, - variadic: false, - description: '' + _required: false, + _variadic: false, + _description: '' }; expect(argument).toEqual(expectedShape); }); @@ -40,9 +40,9 @@ test.each(getSingleArgCases(''))('when add "" usin const argument = cmd._args[0]; const expectedShape = { _name: 'explicit-required', - required: true, - variadic: true, - description: '' + _required: true, + _variadic: true, + _description: '' }; expect(argument).toEqual(expectedShape); }); @@ -51,9 +51,9 @@ test.each(getSingleArgCases('implicit-required...'))('when add "arg..." using %s const argument = cmd._args[0]; const expectedShape = { _name: 'implicit-required', - required: true, - variadic: true, - description: '' + _required: true, + _variadic: true, + _description: '' }; expect(argument).toEqual(expectedShape); }); @@ -62,9 +62,9 @@ test.each(getSingleArgCases('[optional...]'))('when add "[arg...]" using %s then const argument = cmd._args[0]; const expectedShape = { _name: 'optional', - required: false, - variadic: true, - description: '' + _required: false, + _variadic: true, + _description: '' }; expect(argument).toEqual(expectedShape); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index be892dade..559d6289f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -21,9 +21,9 @@ declare namespace commander { type InvalidOptionArgumentErrorConstructor = new (message: string) => InvalidOptionArgumentError; interface Argument { - description: string; - required: boolean; - variadic: boolean; + // description: string; + // required: boolean; + // variadic: boolean; /** * Set the name of the argument. diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 192822c1a..259d69d76 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -335,9 +335,9 @@ expectType(baseOption.attributeName()); // Argument properties const baseArgument = new commander.Argument('(baseArgument.description); -expectType(baseArgument.required); -expectType(baseArgument.variadic); +// expectType(baseArgument.description); +// expectType(baseArgument.required); +// expectType(baseArgument.variadic); // Argument methods // name From b7def8aebe87a94fbdf2044207907c69a15840db Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 30 Mar 2021 20:58:22 +1300 Subject: [PATCH 29/34] Argument should follow Option for properties, make public again --- index.js | 27 +++++++++--------- tests/command.argumentVariations.test.js | 36 ++++++++++++------------ typings/index.d.ts | 6 ++-- typings/index.test-d.ts | 6 ++-- 4 files changed, 37 insertions(+), 38 deletions(-) diff --git a/index.js b/index.js index aaf754fbb..40566d0bf 100644 --- a/index.js +++ b/index.js @@ -87,11 +87,11 @@ class Help { visibleArguments(cmd) { // If there are some argument description then return all the arguments. - if (cmd._argsDescription || cmd._args.find(argument => argument._description)) { + 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()] || ''; + const description = argument.description || legacyDescriptions[argument.name()] || ''; return { term, description }; }); }; @@ -359,27 +359,26 @@ class Argument { */ constructor(name, description) { - this._required = false; - this._variadic = false; - this._name = ''; - this._description = description || ''; + this.description = description || ''; + this.variadic = false; switch (name[0]) { case '<': // e.g. - this._required = true; + this.required = true; this._name = name.slice(1, -1); break; case '[': // e.g. [optional] + this.required = false; this._name = name.slice(1, -1); break; default: + this.required = true; this._name = name; - this._required = true; break; } if (this._name.length > 3 && this._name.slice(-3) === '...') { - this._variadic = true; + this.variadic = true; this._name = this._name.slice(0, -3); } } @@ -865,7 +864,7 @@ class Command extends EventEmitter { */ addArgument(argument) { const previousArgument = this._args.slice(-1)[0]; - if (previousArgument && previousArgument._variadic) { + if (previousArgument && previousArgument.variadic) { throw new Error(`only the last argument can be variadic '${previousArgument.name()}'`); } this._args.push(argument); @@ -1546,9 +1545,9 @@ class Command extends EventEmitter { // Check expected arguments and collect variadic together. const args = this.args.slice(); this._args.forEach((arg, i) => { - if (arg._required && args[i] == null) { + if (arg.required && args[i] == null) { this.missingArgument(arg.name()); - } else if (arg._variadic) { + } else if (arg.variadic) { args[i] = args.splice(i); args.length = Math.min(i + 1, args.length); } @@ -2200,9 +2199,9 @@ function outputHelpIfRequested(cmd, args) { */ function humanReadableArgName(arg) { - const nameOutput = arg.name() + (arg._variadic === true ? '...' : ''); + const nameOutput = arg.name() + (arg.variadic === true ? '...' : ''); - return arg._required + return arg.required ? '<' + nameOutput + '>' : '[' + nameOutput + ']'; } diff --git a/tests/command.argumentVariations.test.js b/tests/command.argumentVariations.test.js index 2ea805a59..a8a781625 100644 --- a/tests/command.argumentVariations.test.js +++ b/tests/command.argumentVariations.test.js @@ -7,9 +7,9 @@ test.each(getSingleArgCases(''))('when add "" using %s t const argument = cmd._args[0]; const expectedShape = { _name: 'explicit-required', - _required: true, - _variadic: false, - _description: '' + required: true, + variadic: false, + description: '' }; expect(argument).toEqual(expectedShape); }); @@ -18,9 +18,9 @@ test.each(getSingleArgCases('implicit-required'))('when add "arg" using %s then const argument = cmd._args[0]; const expectedShape = { _name: 'implicit-required', - _required: true, - _variadic: false, - _description: '' + required: true, + variadic: false, + description: '' }; expect(argument).toEqual(expectedShape); }); @@ -29,9 +29,9 @@ test.each(getSingleArgCases('[optional]'))('when add "[arg]" using %s then argum const argument = cmd._args[0]; const expectedShape = { _name: 'optional', - _required: false, - _variadic: false, - _description: '' + required: false, + variadic: false, + description: '' }; expect(argument).toEqual(expectedShape); }); @@ -40,9 +40,9 @@ test.each(getSingleArgCases(''))('when add "" usin const argument = cmd._args[0]; const expectedShape = { _name: 'explicit-required', - _required: true, - _variadic: true, - _description: '' + required: true, + variadic: true, + description: '' }; expect(argument).toEqual(expectedShape); }); @@ -51,9 +51,9 @@ test.each(getSingleArgCases('implicit-required...'))('when add "arg..." using %s const argument = cmd._args[0]; const expectedShape = { _name: 'implicit-required', - _required: true, - _variadic: true, - _description: '' + required: true, + variadic: true, + description: '' }; expect(argument).toEqual(expectedShape); }); @@ -62,9 +62,9 @@ test.each(getSingleArgCases('[optional...]'))('when add "[arg...]" using %s then const argument = cmd._args[0]; const expectedShape = { _name: 'optional', - _required: false, - _variadic: true, - _description: '' + required: false, + variadic: true, + description: '' }; expect(argument).toEqual(expectedShape); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index 559d6289f..be892dade 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -21,9 +21,9 @@ declare namespace commander { type InvalidOptionArgumentErrorConstructor = new (message: string) => InvalidOptionArgumentError; interface Argument { - // description: string; - // required: boolean; - // variadic: boolean; + description: string; + required: boolean; + variadic: boolean; /** * Set the name of the argument. diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 259d69d76..192822c1a 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -335,9 +335,9 @@ expectType(baseOption.attributeName()); // Argument properties const baseArgument = new commander.Argument('(baseArgument.description); -// expectType(baseArgument.required); -// expectType(baseArgument.variadic); +expectType(baseArgument.description); +expectType(baseArgument.required); +expectType(baseArgument.variadic); // Argument methods // name From f54d1edce8c7b77afd779b13c2ce2d38d331085f Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 30 Mar 2021 21:55:43 +1300 Subject: [PATCH 30/34] Generate help for arguments using same methods as for option and subcommand --- index.js | 46 ++++++++++++++++++++++------- tests/help.visibleArguments.test.js | 4 ++- typings/index.d.ts | 6 +++- typings/index.test-d.ts | 5 +++- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 40566d0bf..92d9a30b9 100644 --- a/index.js +++ b/index.js @@ -79,21 +79,23 @@ class Help { } /** - * Get an array of the arguments which have descriptions. + * Get an array of the arguments if any have a description. * * @param {Command} cmd - * @returns {{ term: string, description:string }[]} + * @returns {Argument[]} */ visibleArguments(cmd) { - // 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 }; + // Side effect! Apply the legacy descriptions before the arguments are displayed. + if (cmd._argsDescription) { + cmd._args.forEach(argument => { + argument.description = argument.description || cmd._argsDescription[argument.name()] || ''; }); + } + + // If there are any arguments with a description then return all the arguments. + if (cmd._args.find(argument => argument.description)) { + return cmd._args; }; return []; } @@ -125,6 +127,17 @@ class Help { return option.flags; } + /** + * Get the argument term to show in the list of arguments. + * + * @param {Argument} argument + * @returns {string} + */ + + argumentTerm(argument) { + return argument.name(); + } + /** * Get the longest command term length. * @@ -163,7 +176,7 @@ class Help { longestArgumentTermLength(cmd, helper) { return helper.visibleArguments(cmd).reduce((max, argument) => { - return Math.max(max, argument.term.length); + return Math.max(max, helper.argumentTerm(argument).length); }, 0); }; @@ -237,6 +250,17 @@ class Help { return option.description; }; + /** + * Get the argument description to show in the list of arguments. + * + * @param {Argument} argument + * @return {string} + */ + + argumentDescription(argument) { + return argument.description; + } + /** * Generate the built-in help text. * @@ -272,7 +296,7 @@ class Help { // Arguments const argumentList = helper.visibleArguments(cmd).map((argument) => { - return formatItem(argument.term, argument.description); + return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument)); }); if (argumentList.length > 0) { output = output.concat(['Arguments:', formatList(argumentList), '']); diff --git a/tests/help.visibleArguments.test.js b/tests/help.visibleArguments.test.js index ac80aa7fe..b35f73ce4 100644 --- a/tests/help.visibleArguments.test.js +++ b/tests/help.visibleArguments.test.js @@ -21,6 +21,8 @@ describe('visibleArguments', () => { const program = new commander.Command(); program.argument('', 'file description'); const helper = new commander.Help(); - expect(helper.visibleArguments(program)).toEqual([{ term: 'file', description: 'file description' }]); + const visibleArguments = helper.visibleArguments(program); + expect(visibleArguments.length).toEqual(1); + expect(visibleArguments[0]).toEqual(new commander.Argument('', 'file description')); }); }); diff --git a/typings/index.d.ts b/typings/index.d.ts index be892dade..f02f547cd 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -119,6 +119,10 @@ declare namespace commander { optionTerm(option: Option): string; /** Get the option description to show in the list of options. */ optionDescription(option: Option): string; + /** Get the argument term to show in the list of arguments. */ + argumentTerm(argument: Argument): string; + /** Get the argument description to show in the list of arguments. */ + argumentDescription(argument: Argument): string; /** Get the command usage to be displayed at the top of the built-in help. */ commandUsage(cmd: Command): string; @@ -130,7 +134,7 @@ declare namespace commander { /** Get an array of the visible options. Includes a placeholder for the implicit help option, if there is one. */ visibleOptions(cmd: Command): Option[]; /** Get an array of the arguments which have descriptions. */ - visibleArguments(cmd: Command): Array<{ term: string; description: string}>; + visibleArguments(cmd: Command): Argument[]; /** Get the longest command term length. */ longestSubcommandTermLength(cmd: Command, helper: Help): number; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 192822c1a..d64b33324 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -272,6 +272,7 @@ expectType(program.configureOutput({ const helper = new commander.Help(); const helperCommand = new commander.Command(); const helperOption = new commander.Option('-a, --all'); +const helperArgument = new commander.Argument(''); expectType(helper.helpWidth); expectType(helper.sortSubcommands); @@ -283,10 +284,12 @@ expectType(helper.commandDescription(helperCommand)); expectType(helper.subcommandDescription(helperCommand)); expectType(helper.optionTerm(helperOption)); expectType(helper.optionDescription(helperOption)); +expectType(helper.argumentTerm(helperArgument)); +expectType(helper.argumentDescription(helperArgument)); expectType(helper.visibleCommands(helperCommand)); expectType(helper.visibleOptions(helperCommand)); -expectType>(helper.visibleArguments(helperCommand)); +expectType(helper.visibleArguments(helperCommand)); expectType(helper.longestSubcommandTermLength(helperCommand, helper)); expectType(helper.longestOptionTermLength(helperCommand, helper)); From 0879a1f60022a41ad3e98e72011db9cf06dabfcb Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 30 Mar 2021 22:16:31 +1300 Subject: [PATCH 31/34] Simplify Argument .name(), just getter --- index.js | 11 ++++------- typings/index.d.ts | 8 +------- typings/index.test-d.ts | 1 - 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 92d9a30b9..9c56eab07 100644 --- a/index.js +++ b/index.js @@ -408,16 +408,13 @@ class Argument { } /** - * Get or set the name of the argument. + * Return argument name. * - * @param {string} [str] - * @return {string|Argument} + * @return {string} */ - name(str) { - if (str === undefined) return this._name; - this._name = str; - return this; + name() { + return this._name; }; } diff --git a/typings/index.d.ts b/typings/index.d.ts index f02f547cd..4fafd0574 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -25,14 +25,8 @@ declare namespace commander { required: boolean; variadic: boolean; - /** - * Set the name of the argument. - * - * @returns `this` command for chaining - */ - name(str: string): this; /** - * Get the name of the argument. + * Return argument name. */ name(): string; } diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index d64b33324..b251b0a96 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -345,4 +345,3 @@ expectType(baseArgument.variadic); // Argument methods // name expectType(baseArgument.name()); -expectType(baseArgument.name('')); From 1e668218631f68cbbec6a25de15fd710093c4626 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 30 Mar 2021 22:39:04 +1300 Subject: [PATCH 32/34] Expand test coverage for visibleArguments --- tests/help.visibleArguments.test.js | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/help.visibleArguments.test.js b/tests/help.visibleArguments.test.js index b35f73ce4..c00be31b8 100644 --- a/tests/help.visibleArguments.test.js +++ b/tests/help.visibleArguments.test.js @@ -25,4 +25,41 @@ describe('visibleArguments', () => { expect(visibleArguments.length).toEqual(1); expect(visibleArguments[0]).toEqual(new commander.Argument('', 'file description')); }); + + test('when argument and legacy argument description then returned', () => { + const program = new commander.Command(); + program.argument(''); + program.description('', { + file: 'file description' + }); + const helper = new commander.Help(); + const visibleArguments = helper.visibleArguments(program); + expect(visibleArguments.length).toEqual(1); + expect(visibleArguments[0]).toEqual(new commander.Argument('', 'file description')); + }); + + test('when arguments and some described then all returned', () => { + const program = new commander.Command(); + program.argument('', 'file1 description'); + program.argument(''); + const helper = new commander.Help(); + const visibleArguments = helper.visibleArguments(program); + expect(visibleArguments.length).toEqual(2); + expect(visibleArguments[0]).toEqual(new commander.Argument('', 'file1 description')); + expect(visibleArguments[1]).toEqual(new commander.Argument('')); + }); + + test('when arguments and some legacy described then all returned', () => { + const program = new commander.Command(); + program.argument(''); + program.argument(''); + program.description('', { + file1: 'file1 description' + }); + const helper = new commander.Help(); + const visibleArguments = helper.visibleArguments(program); + expect(visibleArguments.length).toEqual(2); + expect(visibleArguments[0]).toEqual(new commander.Argument('', 'file1 description')); + expect(visibleArguments[1]).toEqual(new commander.Argument('')); + }); }); From ed7a1c8032aa51442535450b6ee639fdc0b4ab5f Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 5 Apr 2021 14:11:46 +1200 Subject: [PATCH 33/34] Rework the multiple ways of specifying command-arguments --- Readme.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/Readme.md b/Readme.md index 9331c8ee4..af82fd23b 100644 --- a/Readme.md +++ b/Readme.md @@ -394,7 +394,7 @@ $ custom --list x,y,z You can specify (sub)commands using `.command()` or `.addCommand()`. There are two ways these can be implemented: using an action handler attached to the command, or as a stand-alone executable file (described in more detail later). The subcommands may be nested ([example](./examples/nestedCommands.js)). -In the first parameter to `.command()` you specify the command name and any command-arguments. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. +In the first parameter to `.command()` you specify the command name. You may append the command-arguments after the command name, or specify them separately using `.argument()`. The arguments may be `` or `[optional]`, and the last argument may also be `variadic...`. You can use `.addCommand()` to add an already configured subcommand to the program. @@ -410,7 +410,7 @@ program console.log('clone command called'); }); -// Command implemented using stand-alone executable file (description is second parameter to `.command`) +// Command implemented using stand-alone executable file, indicated by adding description as second parameter to `.command`. // Returns `this` for adding more commands. program .command('start ', 'start named service') @@ -428,9 +428,12 @@ subcommand is specified ([example](./examples/defaultCommand.js)). ### Specify the argument syntax -You use `.argument` to specify the expected command-arguments. -Angle brackets around the name indicate a required command-argument (e.g. ``). -Square brackets indicate an optional command-argument (e.g. `[optional]`). The description parameter is optional. +For subcommands, you can specify the argument syntax in the call to `.command()` (as shown above). This +is the only method usable for subcommands implemented using a stand-alone executable, but for other subcommands +you can instead use the following method. + +To configure a command, you can use `.argument()` to specify each expected command-argument. +You supply the argument name and an optional description. The argument may be `` or `[optional]`. Example file: [argument.js](./examples/argument.js) @@ -445,24 +448,14 @@ program }); ``` -There are two ways to specify all the arguments at once, but these do not allow argument descriptions. - -```js -program - .command('add '); - -program - .command('fraction') - .arguments(' '); -``` - 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: + append `...` to the argument name. A variadic argument is passed to the action handler as an array. For example: ```js program .version('0.1.0') - .command('rmdir ') + .command('rmdir') + .argument('') .action(function (dirs) { dirs.forEach((dir) => { console.log('rmdir %s', dir); @@ -470,7 +463,12 @@ program }); ``` -A variadic argument is passed to the action handler as an array. +There is a convenience method to add multiple arguments at once, but without descriptions: + +```js +program + .arguments(' '); +``` ### Action handler From 819616f1ae9c84227278b5ee7c8e15e381033dbb Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 7 Apr 2021 20:57:11 +1200 Subject: [PATCH 34/34] Add Argument to esm exports (and createOption) --- esm.mjs | 2 +- tests/esm-imports-test.mjs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esm.mjs b/esm.mjs index 60bf25212..2206bddd4 100644 --- a/esm.mjs +++ b/esm.mjs @@ -1,4 +1,4 @@ import commander from './index.js'; // wrapper to provide named exports for ESM. -export const { program, Option, Command, CommanderError, InvalidOptionArgumentError, Help, createCommand } = commander; +export const { program, Option, Command, Argument, CommanderError, InvalidOptionArgumentError, Help, createCommand, createOption } = commander; diff --git a/tests/esm-imports-test.mjs b/tests/esm-imports-test.mjs index 1f311590a..222da7777 100644 --- a/tests/esm-imports-test.mjs +++ b/tests/esm-imports-test.mjs @@ -1,4 +1,4 @@ -import { program, Command, Option, CommanderError, InvalidOptionArgumentError, Help, createCommand } from '../esm.mjs'; +import { program, Command, Option, Argument, CommanderError, InvalidOptionArgumentError, Help, createCommand, createOption } from '../esm.mjs'; // Do some simple checks that expected imports are available at runtime. // Run using `npm run test-esm`. @@ -26,8 +26,11 @@ checkClass(new Option('-e, --example'), 'Option'); checkClass(new CommanderError(1, 'code', 'failed'), 'CommanderError'); checkClass(new InvalidOptionArgumentError('failed'), 'InvalidOptionArgumentError'); checkClass(new Help(), 'Help'); +checkClass(new Argument(''), 'Argument'); console.log('Checking createCommand'); check(typeof createCommand === 'function', 'createCommand is function'); +console.log('Checking createOption'); +check(typeof createOption === 'function', 'createOption is function'); console.log('No problems');