From b286da9c5ae889df1ae00ca0c607695d00f89ad2 Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 8 Apr 2021 19:22:17 +1200 Subject: [PATCH] More consistent way of adding command arguments (#1490) * added addArguments method * fix undefined args * added tests, docs and typings * code review fixes * throw error on bad arg * Parse command-argument details in constructor * Handle argument descriptions separately for legacy and new support * Add text to distinguish test names with .each * Match nameAndArgs into two parts, rather than split on spaces. * Update release date post-release to be more accurate * Fix test naming * Simplify and tidy argument example * Typing and typings test for .argument * Expand argument section to include existing multiple-argument approaches. * Add name method and improve Argument typings and tests * Fix copy-and-paste JSDoc error * Update example to match new method and README * Deprecate old way of adding command argument descriptions * Be lenient about Argument construction to allow lazy building * Call first param to .argument "name", and expand jsdoc * Add low-level check that get same Argument from multiple ways of specifying argument * Minor wording tweaks * Add low-level tests for multiple arg variations * Simplify test. Use .argument now. * Restore simple test, use .argument * Big switch from .arguments to .argument in tests * Expand help to explain argument variations * Keep Argument properties private for now (like Command, unlike Option) * Argument should follow Option for properties, make public again * Generate help for arguments using same methods as for option and subcommand * Simplify Argument .name(), just getter * Expand test coverage for visibleArguments * Rework the multiple ways of specifying command-arguments * Add Argument to esm exports (and createOption) Co-authored-by: Nir Yosef --- CHANGELOG.md | 2 +- Readme.md | 39 ++-- docs/deprecated.md | 25 ++ docs/options-taking-varying-arguments.md | 4 +- ...60\347\232\204\351\200\211\351\241\271.md" | 4 +- esm.mjs | 2 +- examples/argument.js | 24 ++ examples/arguments.js | 2 +- examples/pass-through-options.js | 3 +- examples/thank.js | 2 +- index.js | 214 ++++++++++++------ tests/argument.variadic.test.js | 84 +++++++ tests/command.action.test.js | 49 ++-- tests/command.allowExcessArguments.test.js | 8 +- tests/command.allowUnknownOption.test.js | 2 +- tests/command.argumentVariations.test.js | 104 +++++++++ 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/command.usage.test.js | 8 +- tests/commander.configureCommand.test.js | 6 +- tests/esm-imports-test.mjs | 5 +- tests/help.commandTerm.test.js | 4 +- tests/help.commandUsage.test.js | 2 +- tests/help.longestArgumentTermLength.test.js | 12 +- tests/help.padWidth.test.js | 9 +- tests/help.visibleArguments.test.js | 46 +++- tests/options.variadic.test.js | 6 +- typings/index.d.ts | 54 ++++- typings/index.test-d.ts | 20 +- 33 files changed, 622 insertions(+), 189 deletions(-) create mode 100644 examples/argument.js create mode 100644 tests/argument.variadic.test.js create mode 100644 tests/command.argumentVariations.test.js 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 diff --git a/Readme.md b/Readme.md index d2a88a7b2..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,34 +428,34 @@ 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 -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()`. +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. -Example file: [arguments.js](./examples/arguments.js) +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) ```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'); + console.log('password:', password || 'no password given'); }); ``` 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); @@ -463,7 +463,12 @@ program }); ``` -The 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 @@ -474,7 +479,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/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/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/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/examples/argument.js b/examples/argument.js new file mode 100644 index 000000000..83c779eae --- /dev/null +++ b/examples/argument.js @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +// This example shows specifying the arguments using argument() function. + +// const { Command } = require('commander'); // (normal include) +const { Command } = require('../'); // include commander in git clone of commander repo +const program = new Command(); + +program + .version('0.1.0') + .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('password:', 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/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(); 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/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) => { diff --git a/index.js b/index.js index 985a8cfc8..9c56eab07 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 [, helpName, helpArgs] = cmd._helpCommandnameAndArgs.match(/([^ ]+) *(.*)/); + const helpCommand = cmd.createCommand(helpName) .helpOption(false); helpCommand.description(cmd._helpCommandDescription); - helpCommand._parseExpectedArgs(args); + if (helpArgs) helpCommand.arguments(helpArgs); visibleCommands.push(helpCommand); } if (this.sortSubcommands) { @@ -79,18 +79,24 @@ 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 (cmd._argsDescription && cmd._args.length) { - return cmd._args.map((argument) => { - return { term: argument.name, description: cmd._argsDescription[argument.name] || '' }; - }, 0); + // 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 []; } @@ -121,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. * @@ -159,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); }; @@ -233,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. * @@ -268,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), '']); @@ -344,6 +372,52 @@ class Help { } } +class Argument { + /** + * 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} name + * @param {string} [description] + */ + + constructor(name, description) { + this.description = description || ''; + this.variadic = false; + + switch (name[0]) { + case '<': // e.g. + 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; + break; + } + + if (this._name.length > 3 && this._name.slice(-3) === '...') { + this.variadic = true; + this._name = this._name.slice(0, -3); + } + } + + /** + * Return argument name. + * + * @return {string} + */ + + name() { + return this._name; + }; +} + class Option { /** * Initialize a new `Option` with the given `flags` and `description`. @@ -566,7 +640,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; @@ -626,8 +700,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); @@ -654,8 +728,8 @@ class Command extends EventEmitter { cmd._enablePositionalOptions = this._enablePositionalOptions; cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor + if (args) cmd.arguments(args); this.commands.push(cmd); - cmd._parseExpectedArgs(args); cmd.parent = this; if (desc) return this; @@ -763,13 +837,61 @@ 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(desc) { - return this._parseExpectedArgs(desc.split(/ +/)); + arguments(names) { + names.split(/ +/).forEach((detail) => { + this.argument(detail); + }); + return this; }; + /** + * 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]; + if (previousArgument && previousArgument.variadic) { + throw new Error(`only the last argument can be variadic '${previousArgument.name()}'`); + } + this._args.push(argument); + return this; + } + /** * Override default decision whether to add implicit help command. * @@ -806,51 +928,6 @@ class Command extends EventEmitter { return this._addImplicitHelpCommand; }; - /** - * Parse expected `args`. - * - * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. - * - * @param {Array} args - * @return {Command} `this` command for chaining - * @api private - */ - - _parseExpectedArgs(args) { - if (!args.length) return; - args.forEach((arg) => { - const argDetails = { - required: false, - name: '', - variadic: false - }; - - switch (arg[0]) { - case '<': - argDetails.required = true; - argDetails.name = arg.slice(1, -1); - break; - case '[': - argDetails.name = arg.slice(1, -1); - break; - } - - if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') { - argDetails.variadic = true; - argDetails.name = argDetails.name.slice(0, -3); - } - if (argDetails.name) { - this._args.push(argDetails); - } - }); - this._args.forEach((arg, i) => { - if (arg.variadic && i < this._args.length - 1) { - throw new Error(`only the last argument can be variadic '${arg.name}'`); - } - }); - return this; - }; - /** * Register callback to use as replacement for calling process.exit. * @@ -1490,7 +1567,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); @@ -1851,7 +1928,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; }; @@ -2096,6 +2175,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; @@ -2134,13 +2214,13 @@ 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 ? '<' + nameOutput + '>' 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..765dddf0c 100644 --- a/tests/command.action.test.js +++ b/tests/command.action.test.js @@ -23,23 +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 via %s and argument supplied then action called', (methodName, program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('') - .action(actionMock); + 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 via %s and argument not supplied then action not called', (methodName, program) => { const actionMock = jest.fn(); - const program = new commander.Command(); program .exitOverride() .configureOutput({ writeErr: () => {} }) - .arguments('') .action(actionMock); expect(() => { program.parse(['node', 'test']); @@ -57,32 +52,23 @@ 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 via %s supplied then action called', (methodName, program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('[file]') - .action(actionMock); + 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', (methodName, program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('[file]') - .action(actionMock); + program.action(actionMock); program.parse(['node', 'test']); 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 via %s and subcommand and program argument then program action called', (methodName, program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('[file]') - .action(actionMock); + program.action(actionMock); program .command('subcommand'); @@ -92,14 +78,10 @@ test('when .action on program with optional argument and subcommand and 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 via %s and subcommand and no program argument then program action called', (methodName, program) => { const actionMock = jest.fn(); - const program = new commander.Command(); - program - .arguments('[file]') - .action(actionMock); - program - .command('subcommand'); + program.action(actionMock); + program.command('subcommand'); program.parse(['node', 'test']); @@ -121,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 [['.arguments', withArguments], ['.argument', withArgument], ['.addArgument', withAddArgument]]; +} 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.argumentVariations.test.js b/tests/command.argumentVariations.test.js new file mode 100644 index 000000000..a8a781625 --- /dev/null +++ b/tests/command.argumentVariations.test.js @@ -0,0 +1,104 @@ +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(getSingleArgCases(''))('when add "" using %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(getSingleArgCases('implicit-required'))('when add "arg" using %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(getSingleArgCases('[optional]'))('when add "[arg]" using %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(getSingleArgCases(''))('when add "" using %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(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', + required: true, + variadic: true, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +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', + required: false, + variadic: true, + description: '' + }; + expect(argument).toEqual(expectedShape); +}); + +function getSingleArgCases(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}`)] + ]; +} + +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']); +}); 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/command.usage.test.js b/tests/command.usage.test.js index 8cae3ad9f..eee4d9bfc 100644 --- a/tests/command.usage.test.js +++ b/tests/command.usage.test.js @@ -78,20 +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(''); 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'); 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/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'); 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.commandUsage.test.js b/tests/help.commandUsage.test.js index abeaceba0..08cd776bd 100644 --- a/tests/help.commandUsage.test.js +++ b/tests/help.commandUsage.test.js @@ -42,7 +42,7 @@ describe('commandUsage', () => { const program = new commander.Command(); program .name('program') - .arguments(''); + .argument(''); const helper = new commander.Help(); expect(helper.commandUsage(program)).toEqual('program [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..c00be31b8 100644 --- a/tests/help.visibleArguments.test.js +++ b/tests/help.visibleArguments.test.js @@ -12,16 +12,54 @@ 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' }]); + const visibleArguments = helper.visibleArguments(program); + 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('')); }); }); 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(); diff --git a/typings/index.d.ts b/typings/index.d.ts index ee40d8990..4fafd0574 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -20,6 +20,17 @@ declare namespace commander { } type InvalidOptionArgumentErrorConstructor = new (message: string) => InvalidOptionArgumentError; + interface Argument { + description: string; + required: boolean; + variadic: boolean; + + /** + * Return argument name. + */ + name(): string; + } + interface Option { flags: string; description: string; @@ -86,6 +97,7 @@ declare namespace commander { attributeName(): 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 */ @@ -101,6 +113,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; @@ -112,7 +128,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; @@ -236,9 +252,37 @@ 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. + * + * @example + * + * program.argument(''); + * program.argument('[output-file]'); + * * @returns `this` command for chaining */ - arguments(desc: string): this; + 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; /** * Override default decision whether to add implicit help command. @@ -492,7 +536,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. */ @@ -614,6 +661,7 @@ declare namespace commander { program: Command; Command: CommandConstructor; Option: OptionConstructor; + Argument: ArgumentConstructor; CommanderError: CommanderErrorConstructor; InvalidOptionArgumentError: InvalidOptionArgumentErrorConstructor; Help: HelpConstructor; diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index ca6ee0a7f..b251b0a96 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]')); @@ -174,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')); @@ -267,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); @@ -278,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)); @@ -327,3 +335,13 @@ 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());