diff --git a/CHANGELOG.md b/CHANGELOG.md index b976f4f77..7af0f9323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,31 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. +## [6.1.0] (2020-08-28) + +### Added + +- include URL to relevant section of README for error for potential conflict between Command properties and option values ([#1306]) +- `.combineFlagAndOptionalValue(false)` to ease upgrade path from older versions of Commander ([#1326]) +- allow disabling the built-in help option using `.helpOption(false)` ([#1325]) +- allow just some arguments in `argumentDescription` to `.description()` ([#1323]) + +### Changed + +- tidy async test and remove lint override ([#1312]) + +### Fixed + +- executable subcommand launching when script path not known ([#1322]) + ## [6.0.0] (2020-07-21) ### Added - add support for variadic options ([#1250]) - allow options to be added with just a short flag ([#1256]) -- throw an error if there might be a clash between option name and a Command property, with advice on how to resolve ([#1275]) + - *Breaking* the option property has same case as flag. e.g. flag `-n` accessed as `opts().n` (previously uppercase) +- *Breaking* throw an error if there might be a clash between option name and a Command property, with advice on how to resolve ([#1275]) ### Fixed @@ -113,6 +131,9 @@ If you use `program.args` or more complicated tests to detect a missing subcomma If you use `.command('*')` to add a default command, you may be be able to switch to `isDefault:true` with a named command. +If you want to continue combining short options with optional values as though they were boolean flags, set `combineFlagAndOptionalValue(false)` +to expand `-fb` to `-f -b` rather than `-f b`. + ## [5.0.0-4] (2020-03-03) (Released in 5.0.0) @@ -281,8 +302,15 @@ if (program.rawArgs.length < 3) ... [#1256]: https://github.com/tj/commander.js/pull/1256 [#1275]: https://github.com/tj/commander.js/pull/1275 [#1301]: https://github.com/tj/commander.js/issues/1301 +[#1306]: https://github.com/tj/commander.js/pull/1306 +[#1312]: https://github.com/tj/commander.js/pull/1312 +[#1322]: https://github.com/tj/commander.js/pull/1322 +[#1323]: https://github.com/tj/commander.js/pull/1323 +[#1325]: https://github.com/tj/commander.js/pull/1325 +[#1326]: https://github.com/tj/commander.js/pull/1326 [Unreleased]: https://github.com/tj/commander.js/compare/master...develop +[6.1.0]: https://github.com/tj/commander.js/compare/v6.0.0..v6.1.0 [6.0.0]: https://github.com/tj/commander.js/compare/v5.1.0..v6.0.0 [6.0.0-0]: https://github.com/tj/commander.js/compare/v5.1.0..v6.0.0-0 [5.1.0]: https://github.com/tj/commander.js/compare/v5.0.0..v5.1.0 diff --git a/Readme.md b/Readme.md index 577a16396..a1d389b82 100644 --- a/Readme.md +++ b/Readme.md @@ -376,18 +376,11 @@ program .version('0.1.0') .arguments(' [env]') .action(function (cmd, env) { - cmdValue = cmd; - envValue = env; + console.log('command:', cmdValue); + console.log('environment:', envValue || 'no environment given'); }); program.parse(process.argv); - -if (typeof cmdValue === 'undefined') { - console.error('no command given!'); - process.exit(1); -} -console.log('command:', cmdValue); -console.log('environment:', envValue || "no environment given"); ``` The last argument of a command can be variadic, and only the last argument. To make an argument variadic you @@ -566,7 +559,7 @@ from `--help` listeners.) ### .helpOption(flags, description) -Override the default help flags and description. +Override the default help flags and description. Pass false to disable the built-in help option. ```js program diff --git a/examples/env b/examples/env index e6b62034a..ba91bfdc4 100755 --- a/examples/env +++ b/examples/env @@ -12,16 +12,9 @@ let envValue; program .version('0.0.1') .arguments(' [env]') - .action(function(cmd, env) { - cmdValue = cmd; - envValue = env; + .action(function(cmdValue, envValue) { + console.log('command:', cmdValue); + console.log('environment:', envValue || 'no environment given'); }); program.parse(process.argv); - -if (typeof cmdValue === 'undefined') { - console.error('no command given!'); - process.exit(1); -} -console.log('command:', cmdValue); -console.log('environment:', envValue || 'no environment given'); diff --git a/index.js b/index.js index e73254f7f..c1b67b7b2 100644 --- a/index.js +++ b/index.js @@ -127,8 +127,10 @@ class Command extends EventEmitter { this._defaultCommandName = null; this._exitCallback = null; this._aliases = []; + this._combineFlagAndOptionalValue = true; this._hidden = false; + this._hasHelpOption = true; this._helpFlags = '-h, --help'; this._helpDescription = 'display help for command'; this._helpShortFlag = '-h'; @@ -184,6 +186,7 @@ class Command extends EventEmitter { if (opts.isDefault) this._defaultCommandName = cmd._name; cmd._hidden = !!(opts.noHelp || opts.hidden); + cmd._hasHelpOption = this._hasHelpOption; cmd._helpFlags = this._helpFlags; cmd._helpDescription = this._helpDescription; cmd._helpShortFlag = this._helpShortFlag; @@ -194,6 +197,7 @@ class Command extends EventEmitter { cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._passCommandToAction = this._passCommandToAction; + cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue; cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor this.commands.push(cmd); @@ -468,7 +472,9 @@ class Command extends EventEmitter { throw new Error(`option '${option.name()}' clashes with existing property '${option.attributeName()}' on Command - call storeOptionsAsProperties(false) to store option values safely, - or call storeOptionsAsProperties(true) to suppress this check, -- or change option name`); +- or change option name + +Read more on https://git.io/JJc0W`); } }; @@ -634,6 +640,23 @@ class Command extends EventEmitter { return this._optionEx({ mandatory: true }, flags, description, fn, defaultValue); }; + /** + * Alter parsing of short flags with optional values. + * + * Examples: + * + * // for `.option('-f,--flag [value]'): + * .combineFlagAndOptionalValue(true) // `-f80` is treated like `--flag=80`, this is the default behaviour + * .combineFlagAndOptionalValue(false) // `-fb` is treated like `-f -b` + * + * @param {Boolean} [arg] - if `true` or omitted, an optional value can be specified directly after the flag. + * @api public + */ + combineFlagAndOptionalValue(arg) { + this._combineFlagAndOptionalValue = (arg === undefined) || arg; + return this; + }; + /** * Allow unknown options on the command line. * @@ -821,7 +844,11 @@ class Command extends EventEmitter { this._checkForMissingMandatoryOptions(); // Want the entry script as the reference for command name and directory for searching for other files. - const scriptPath = this._scriptPath; + let scriptPath = this._scriptPath; + // Fallback in case not set, due to how Command created or called. + if (!scriptPath && process.mainModule) { + scriptPath = process.mainModule.filename; + } let baseDir; try { @@ -1103,7 +1130,7 @@ class Command extends EventEmitter { if (arg.length > 2 && arg[0] === '-' && arg[1] !== '-') { const option = this._findOption(`-${arg[1]}`); if (option) { - if (option.required || option.optional) { + if (option.required || (option.optional && this._combineFlagAndOptionalValue)) { // option with value following in same argument this.emit(`option:${option.name()}`, arg.slice(2)); } else { @@ -1230,7 +1257,8 @@ class Command extends EventEmitter { partCommands.unshift(parentCmd.name()); } const fullCommand = partCommands.join(' '); - const message = `error: unknown command '${this.args[0]}'. See '${fullCommand} ${this._helpLongFlag}'.`; + const message = `error: unknown command '${this.args[0]}'.` + + (this._hasHelpOption ? ` See '${fullCommand} ${this._helpLongFlag}'.` : ''); console.error(message); this._exit(1, 'commander.unknownCommand', message); }; @@ -1339,9 +1367,11 @@ class Command extends EventEmitter { const args = this._args.map((arg) => { return humanReadableArgName(arg); }); - return '[options]' + - (this.commands.length ? ' [command]' : '') + - (this._args.length ? ' ' + args.join(' ') : ''); + return [].concat( + (this.options.length || this._hasHelpOption ? '[options]' : []), + (this.commands.length ? '[command]' : []), + (this._args.length ? args : []) + ).join(' '); } this._usage = str; @@ -1484,8 +1514,8 @@ class Command extends EventEmitter { }); // Implicit help - const showShortHelpFlag = this._helpShortFlag && !this._findOption(this._helpShortFlag); - const showLongHelpFlag = !this._findOption(this._helpLongFlag); + const showShortHelpFlag = this._hasHelpOption && this._helpShortFlag && !this._findOption(this._helpShortFlag); + const showLongHelpFlag = this._hasHelpOption && !this._findOption(this._helpLongFlag); if (showShortHelpFlag || showLongHelpFlag) { let helpFlags = this._helpFlags; if (!showShortHelpFlag) { @@ -1548,7 +1578,7 @@ class Command extends EventEmitter { desc.push('Arguments:'); desc.push(''); this._args.forEach((arg) => { - desc.push(' ' + pad(arg.name, width) + ' ' + wrap(argsDescription[arg.name], descriptionWidth, width + 4)); + desc.push(' ' + pad(arg.name, width) + ' ' + wrap(argsDescription[arg.name] || '', descriptionWidth, width + 4)); }); desc.push(''); } @@ -1571,11 +1601,14 @@ class Command extends EventEmitter { const commandHelp = this.commandHelp(); if (commandHelp) cmds = [commandHelp]; - const options = [ - 'Options:', - '' + this.optionHelp().replace(/^/gm, ' '), - '' - ]; + let options = []; + if (this._hasHelpOption || this.options.length > 0) { + options = [ + 'Options:', + '' + this.optionHelp().replace(/^/gm, ' '), + '' + ]; + } return usage .concat(desc) @@ -1609,15 +1642,20 @@ class Command extends EventEmitter { /** * You can pass in flags and a description to override the help - * flags and help description for your command. + * flags and help description for your command. Pass in false to + * disable the built-in help option. * - * @param {string} [flags] + * @param {string | boolean} [flags] * @param {string} [description] * @return {Command} `this` command for chaining * @api public */ helpOption(flags, description) { + if (typeof flags === 'boolean') { + this._hasHelpOption = flags; + return this; + } this._helpFlags = flags || this._helpFlags; this._helpDescription = description || this._helpDescription; @@ -1750,7 +1788,7 @@ function optionalWrap(str, width, indent) { */ function outputHelpIfRequested(cmd, args) { - const helpOption = args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag); + const helpOption = cmd._hasHelpOption && args.find(arg => arg === cmd._helpLongFlag || arg === cmd._helpShortFlag); if (helpOption) { cmd.outputHelp(); // (Do not have all displayed text available so only passing placeholder.) diff --git a/package-lock.json b/package-lock.json index 59f2c4674..c8c6bcc0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "commander", - "version": "6.0.0", + "version": "6.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 973f9d0da..d31b5c5bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "commander", - "version": "6.0.0", + "version": "6.1.0", "description": "the complete solution for node.js command-line programs", "keywords": [ "commander", diff --git a/tests/command.exitOverride.test.js b/tests/command.exitOverride.test.js index ecd1f1e46..481b85e16 100644 --- a/tests/command.exitOverride.test.js +++ b/tests/command.exitOverride.test.js @@ -175,19 +175,19 @@ describe('.exitOverride and error details', () => { expectCommanderError(caughtErr, 0, 'commander.version', myVersion); }); - // Have not worked out a cleaner way to do this, so suppress warning. - // eslint-disable-next-line jest/no-test-callback - test('when executableSubcommand succeeds then call exitOverride', (done) => { + test('when executableSubcommand succeeds then call exitOverride', async() => { + expect.hasAssertions(); const pm = path.join(__dirname, 'fixtures/pm'); const program = new commander.Command(); - program - .exitOverride((err) => { - expectCommanderError(err, 0, 'commander.executeSubCommandAsync', '(close)'); - done(); - }) - .command('silent', 'description'); - - program.parse(['node', pm, 'silent']); + await new Promise((resolve) => { + program + .exitOverride((err) => { + expectCommanderError(err, 0, 'commander.executeSubCommandAsync', '(close)'); + resolve(); + }) + .command('silent', 'description'); + program.parse(['node', pm, 'silent']); + }); }); test('when mandatory program option missing then throw CommanderError', () => { diff --git a/tests/command.help.test.js b/tests/command.help.test.js index d6e0e899a..c2ed34b3c 100644 --- a/tests/command.help.test.js +++ b/tests/command.help.test.js @@ -130,3 +130,12 @@ test('when both help flags masked then not displayed in helpInformation', () => const helpInformation = program.helpInformation(); expect(helpInformation).not.toMatch('display help'); }); + +test('when no options then Options not includes in helpInformation', () => { + const program = new commander.Command(); + // No custom options, no version option, no help option + program + .helpOption(false); + const helpInformation = program.helpInformation(); + expect(helpInformation).not.toMatch('Options'); +}); diff --git a/tests/command.helpOption.test.js b/tests/command.helpOption.test.js index 8cda1f20b..35cff54ee 100644 --- a/tests/command.helpOption.test.js +++ b/tests/command.helpOption.test.js @@ -2,18 +2,22 @@ const commander = require('../'); describe('helpOption', () => { let writeSpy; + let consoleErrorSpy; beforeAll(() => { - // Optional. Suppress normal output to keep test output clean. + // Optional. Suppress expected output to keep test output clean. writeSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => { }); + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => { }); }); afterEach(() => { writeSpy.mockClear(); + consoleErrorSpy.mockClear(); }); afterAll(() => { writeSpy.mockRestore(); + consoleErrorSpy.mockRestore(); }); test('when helpOption has custom flags then custom short flag invokes help', () => { @@ -63,4 +67,51 @@ describe('helpOption', () => { const helpInformation = program.helpInformation(); expect(helpInformation).toMatch(/-C,--custom-help +custom help output/); }); + + test('when helpOption(false) then helpInformation does not include --help', () => { + const program = new commander.Command(); + program + .helpOption(false); + const helpInformation = program.helpInformation(); + expect(helpInformation).not.toMatch('--help'); + }); + + test('when helpOption(false) then --help is an unknown option', () => { + const program = new commander.Command(); + program + .exitOverride() + .helpOption(false); + let caughtErr; + try { + program.parse(['--help'], { from: 'user' }); + } catch (err) { + caughtErr = err; + } + expect(caughtErr.code).toBe('commander.unknownOption'); + }); + + test('when helpOption(false) then -h is an unknown option', () => { + const program = new commander.Command(); + program + .exitOverride() + .helpOption(false); + let caughtErr; + try { + program.parse(['-h'], { from: 'user' }); + } catch (err) { + caughtErr = err; + } + expect(caughtErr.code).toBe('commander.unknownOption'); + }); + + test('when helpOption(false) then unknown command error does not suggest --help', () => { + const program = new commander.Command(); + program + .exitOverride() + .helpOption(false) + .command('foo'); + expect(() => { + program.parse(['UNKNOWN'], { from: 'user' }); + }).toThrow("error: unknown command 'UNKNOWN'."); + }); }); diff --git a/tests/command.parseOptions.test.js b/tests/command.parseOptions.test.js index 3615b7fc4..4c8e8480e 100644 --- a/tests/command.parseOptions.test.js +++ b/tests/command.parseOptions.test.js @@ -288,4 +288,25 @@ describe('Utility Conventions', () => { expect(result).toEqual({ operands: [], unknown: ['--rrr=value'] }); expect(program.opts()).toEqual({ }); }); + + test('when program has combo optional and combineFlagAndOptionalValue() then treat as value', () => { + const program = createProgram(); + program.combineFlagAndOptionalValue(); + program.parseOptions(['-db']); + expect(program.opts()).toEqual({ ddd: 'b' }); + }); + + test('when program has combo optional and combineFlagAndOptionalValue(true) then treat as value', () => { + const program = createProgram(); + program.combineFlagAndOptionalValue(true); + program.parseOptions(['-db']); + expect(program.opts()).toEqual({ ddd: 'b' }); + }); + + test('when program has combo optional and combineFlagAndOptionalValue(false) then treat as boolean', () => { + const program = createProgram(); + program.combineFlagAndOptionalValue(false); + program.parseOptions(['-db']); + expect(program.opts()).toEqual({ ddd: true, bbb: true }); + }); }); diff --git a/tests/command.usage.test.js b/tests/command.usage.test.js index 6140ec93b..748404dad 100644 --- a/tests/command.usage.test.js +++ b/tests/command.usage.test.js @@ -44,3 +44,56 @@ test('when custom usage and check subcommand help then starts with custom usage expect(helpInformation).toMatch(new RegExp(`^Usage: test info ${myUsage}`)); }); + +test('when has option then [options] included in usage', () => { + const program = new commander.Command(); + + program + .option('--foo'); + + expect(program.usage()).toMatch('[options]'); +}); + +test('when no options then [options] not included in usage', () => { + const program = new commander.Command(); + + program + .helpOption(false); + + expect(program.usage()).not.toMatch('[options]'); +}); + +test('when has command then [command] included in usage', () => { + const program = new commander.Command(); + + program + .command('foo'); + + expect(program.usage()).toMatch('[command]'); +}); + +test('when no commands then [command] not included in usage', () => { + const program = new commander.Command(); + + expect(program.usage()).not.toMatch('[command]'); +}); + +test('when arguments then arguments included in usage', () => { + const program = new commander.Command(); + + program + .arguments(''); + + expect(program.usage()).toMatch(''); +}); + +test('when options and command and arguments then all three included in usage', () => { + const program = new commander.Command(); + + program + .arguments('') + .option('--alpha') + .command('beta'); + + expect(program.usage()).toEqual('[options] [command] '); +}); diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 57392c667..d7914dd73 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -134,6 +134,10 @@ const storeOptionsAsPropertiesThis2: commander.Command = program.storeOptionsAsP const passCommandToActionThis1: commander.Command = program.passCommandToAction(); const passCommandToActionThis2: commander.Command = program.passCommandToAction(false); +// combineFlagAndOptionalValue +const combineFlagAndOptionalValueThis1: commander.Command = program.combineFlagAndOptionalValue(); +const combineFlagAndOptionalValueThis2: commander.Command = program.combineFlagAndOptionalValue(false); + // allowUnknownOption const allowUnknownOptionThis1: commander.Command = program.allowUnknownOption(); const allowUnknownOptionThis2: commander.Command = program.allowUnknownOption(false); @@ -196,6 +200,7 @@ const helpInformnationValue: string = program.helpInformation(); const helpOptionThis1: commander.Command = program.helpOption('-h,--help'); const helpOptionThis2: commander.Command = program.helpOption('-h,--help', 'custom description'); const helpOptionThis3: commander.Command = program.helpOption(undefined, 'custom description'); +const helpOptionThis4: commander.Command = program.helpOption(false); // on const onThis: commander.Command = program.on('--help', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index c8061ffb3..e404ae88d 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -201,6 +201,18 @@ declare namespace commander { */ passCommandToAction(value?: boolean): this; + /** + * Alter parsing of short flags with optional values. + * + * @example + * // for `.option('-f,--flag [value]'): + * .combineFlagAndOptionalValue(true) // `-f80` is treated like `--flag=80`, this is the default behaviour + * .combineFlagAndOptionalValue(false) // `-fb` is treated like `-f -b` + * + * @returns `this` command for chaining + */ + combineFlagAndOptionalValue(arg?: boolean): this; + /** * Allow unknown options on the command line. * @@ -335,9 +347,10 @@ declare namespace commander { /** * You can pass in flags and a description to override the help - * flags and help description for your command. + * flags and help description for your command. Pass in false + * to disable the built-in help option. */ - helpOption(flags?: string, description?: string): this; + helpOption(flags?: string | boolean, description?: string): this; /** * Output help information and exit.