From 0f74c8b40bb77ca178e817c80dc2881b35e7c7fb Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 27 Dec 2020 23:18:48 +1300 Subject: [PATCH 01/26] First cut at optionsBeforeArguments --- index.js | 15 ++++++ tests/command.optionsBeforeArguments.test.js | 55 ++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/command.optionsBeforeArguments.test.js diff --git a/index.js b/index.js index 73deef361..fa605c8f5 100644 --- a/index.js +++ b/index.js @@ -552,6 +552,7 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; + this._optionsBeforeArguments = false; // see .configureOutput() for docs this._outputConfiguration = { @@ -1614,6 +1615,20 @@ class Command extends EventEmitter { dest = unknown; } + if (!maybeOption(arg) && this._optionsBeforeArguments) { + dest.push(arg); + if (this._findCommand(arg)) { + // sub --debug + unknown.push(...args); + } else { + // help foo + // arg --help + // arg --debug + dest.push(...args); + } + break; + } + // add arg dest.push(arg); } diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js new file mode 100644 index 000000000..acaf6bb2c --- /dev/null +++ b/tests/command.optionsBeforeArguments.test.js @@ -0,0 +1,55 @@ +const commander = require('../'); + +test('when default then global option parsed after command-argument', () => { + const program = new commander.Command(); + program + .option('-d, --debug') + .arguments('[arg]'); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(program.opts().debug).toBe(true); + expect(program.args).toEqual(['arg']); +}); + +test('when optionsBeforeArguments then global option not parsed after command-argument', () => { + const program = new commander.Command(); + program._optionsBeforeArguments = true; + program + .option('-d, --debug') + .arguments('[arg]'); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(program.opts().debug).toBeUndefined(); + expect(program.args).toEqual(['arg', '--debug']); +}); + +test('when default then global option parsed after command', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program + .option('-d, --debug'); + const sub = program.command('sub') + .arguments('[arg]') + .action(mockAction); + program.parse(['sub', '--debug', 'arg'], { from: 'user' }); + expect(program.opts().debug).toBe(true); + expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); +}); + +test('when optionsBeforeArguments then global option after command belongs to command', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program._optionsBeforeArguments = true; + program + .option('-d, --debug'); + const sub = program.command('sub') + .arguments('[arg]') + .option('-d, --debug') + .action(mockAction); + program.parse(['sub', '--debug', 'arg'], { from: 'user' }); + expect(program.opts().debug).toBeUndefined(); + expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); +}); + +// arg --help +// sub --help +// help sub +// default command, arg --debug From 3de2bdeb4b0092edaf3d501b6891dee14f256f0b Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 09:30:58 +1300 Subject: [PATCH 02/26] Different to mix global options and subcommands, and options and arguments. --- tests/command.optionsBeforeArguments.test.js | 48 +++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index acaf6bb2c..7e0b9ebce 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -1,6 +1,6 @@ const commander = require('../'); -test('when default then global option parsed after command-argument', () => { +test('when default then option parsed after command-argument', () => { const program = new commander.Command(); program .option('-d, --debug') @@ -10,18 +10,18 @@ test('when default then global option parsed after command-argument', () => { expect(program.args).toEqual(['arg']); }); -test('when optionsBeforeArguments then global option not parsed after command-argument', () => { - const program = new commander.Command(); - program._optionsBeforeArguments = true; - program - .option('-d, --debug') - .arguments('[arg]'); - program.parse(['arg', '--debug'], { from: 'user' }); - expect(program.opts().debug).toBeUndefined(); - expect(program.args).toEqual(['arg', '--debug']); -}); +// test('when optionsBeforeArguments then global option not parsed after command-argument', () => { +// const program = new commander.Command(); +// program._optionsBeforeArguments = true; +// program +// .option('-d, --debug') +// .arguments('[arg]'); +// program.parse(['arg', '--debug'], { from: 'user' }); +// expect(program.opts().debug).toBeUndefined(); +// expect(program.args).toEqual(['arg', '--debug']); +// }); -test('when default then global option parsed after command', () => { +test('when global option after subcommand and default then global option parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); program @@ -34,22 +34,36 @@ test('when default then global option parsed after command', () => { expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); }); -test('when optionsBeforeArguments then global option after command belongs to command', () => { +test('when global option after subcommand and parseGlobalOptionsAnywhere(false) then global option not parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); - program._optionsBeforeArguments = true; + program._parseGlobalOptionsAnywhere = false; program .option('-d, --debug'); const sub = program.command('sub') .arguments('[arg]') + .allowUnknownOption() + .action(mockAction); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(program.opts().debug).toBeUndefined(); + expect(mockAction).toBeCalledWith('--debug', sub.opts(), sub); +}); + +test('when option after subcommand is global and local and parseGlobalOptionsAnywhere(false) then option parsed as local', () => { + const mockAction = jest.fn(); + const program = new commander.Command(); + program._parseGlobalOptionsAnywhere = false; + program + .option('-d, --debug'); + const sub = program.command('sub') .option('-d, --debug') .action(mockAction); - program.parse(['sub', '--debug', 'arg'], { from: 'user' }); + program.parse(['sub', '--debug'], { from: 'user' }); expect(program.opts().debug).toBeUndefined(); - expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); + expect(sub.opts().debug).toBe(true); }); // arg --help // sub --help // help sub -// default command, arg --debug +// default command, arg From bc1f8f79806fbf622f4286b9e2026d83c2a6cf89 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 09:35:52 +1300 Subject: [PATCH 03/26] Different to mix global options and subcommands, and options and arguments. --- index.js | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index fa605c8f5..65ec94518 100644 --- a/index.js +++ b/index.js @@ -552,7 +552,7 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; - this._optionsBeforeArguments = false; + this._parseGlobalOptionsAnywhere = true; // see .configureOutput() for docs this._outputConfiguration = { @@ -1615,18 +1615,16 @@ class Command extends EventEmitter { dest = unknown; } - if (!maybeOption(arg) && this._optionsBeforeArguments) { - dest.push(arg); - if (this._findCommand(arg)) { - // sub --debug + // found first non-option + if (operands.length === 0 && unknown.length === 0) { + // check whether to stop parsing global options because hit subcommand + if (!this._parseGlobalOptionsAnywhere && this._findCommand(arg)) { + dest.push(arg); unknown.push(...args); - } else { - // help foo - // arg --help - // arg --debug - dest.push(...args); + break; } - break; + // check whether to stop parsing options because hit argument + // ... } // add arg From c93da58429e901c3a5563111920879551381f9c4 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 10:06:10 +1300 Subject: [PATCH 04/26] Add _parseOptionsFollowingArguments --- index.js | 20 +++++++++------- tests/command.optionsBeforeArguments.test.js | 24 ++++++++++---------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/index.js b/index.js index 65ec94518..e9ecf34e2 100644 --- a/index.js +++ b/index.js @@ -552,7 +552,8 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; - this._parseGlobalOptionsAnywhere = true; + this._parseGlobalOptionsAnywhere = true; // before and after subcommands + this._parseOptionsFollowingArguments = true; // see .configureOutput() for docs this._outputConfiguration = { @@ -1610,21 +1611,24 @@ class Command extends EventEmitter { } } - // looks like an option but unknown, unknowns from here - if (arg.length > 1 && arg[0] === '-') { - dest = unknown; - } - // found first non-option if (operands.length === 0 && unknown.length === 0) { // check whether to stop parsing global options because hit subcommand if (!this._parseGlobalOptionsAnywhere && this._findCommand(arg)) { - dest.push(arg); + operands.push(arg); unknown.push(...args); break; } // check whether to stop parsing options because hit argument - // ... + if (!this._parseOptionsFollowingArguments && !this._findCommand(arg)) { + operands.push(arg, ...args); + break; + } + } + + // looks like an option but unknown, unknowns from here + if (arg.length > 1 && arg[0] === '-') { + dest = unknown; } // add arg diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 7e0b9ebce..16985921b 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -1,6 +1,6 @@ const commander = require('../'); -test('when default then option parsed after command-argument', () => { +test('when option after argument then option parsed', () => { const program = new commander.Command(); program .option('-d, --debug') @@ -10,18 +10,18 @@ test('when default then option parsed after command-argument', () => { expect(program.args).toEqual(['arg']); }); -// test('when optionsBeforeArguments then global option not parsed after command-argument', () => { -// const program = new commander.Command(); -// program._optionsBeforeArguments = true; -// program -// .option('-d, --debug') -// .arguments('[arg]'); -// program.parse(['arg', '--debug'], { from: 'user' }); -// expect(program.opts().debug).toBeUndefined(); -// expect(program.args).toEqual(['arg', '--debug']); -// }); +test('when option after argument and _parseOptionsFollowingArguments(false) then option not parsed', () => { + const program = new commander.Command(); + program._parseOptionsFollowingArguments = false; + program + .option('-d, --debug') + .arguments('[arg]'); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(program.opts().debug).toBeUndefined(); + expect(program.args).toEqual(['arg', '--debug']); +}); -test('when global option after subcommand and default then global option parsed', () => { +test('when global option after subcommand then global option parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); program From 69458b44afe9320a330485eb58557cbfda15d9cb Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 10:15:14 +1300 Subject: [PATCH 05/26] Use allow wording --- index.js | 8 ++++---- tests/command.optionsBeforeArguments.test.js | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index e9ecf34e2..a303af0da 100644 --- a/index.js +++ b/index.js @@ -552,8 +552,8 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; - this._parseGlobalOptionsAnywhere = true; // before and after subcommands - this._parseOptionsFollowingArguments = true; + this._allowGlobalOptionsAnywhere = true; // before and after subcommands + this._allowOptionsFollowingArguments = true; // see .configureOutput() for docs this._outputConfiguration = { @@ -1614,13 +1614,13 @@ class Command extends EventEmitter { // found first non-option if (operands.length === 0 && unknown.length === 0) { // check whether to stop parsing global options because hit subcommand - if (!this._parseGlobalOptionsAnywhere && this._findCommand(arg)) { + if (!this._allowGlobalOptionsAnywhere && this._findCommand(arg)) { operands.push(arg); unknown.push(...args); break; } // check whether to stop parsing options because hit argument - if (!this._parseOptionsFollowingArguments && !this._findCommand(arg)) { + if (!this._allowOptionsFollowingArguments && !this._findCommand(arg)) { operands.push(arg, ...args); break; } diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 16985921b..271003da9 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -12,7 +12,7 @@ test('when option after argument then option parsed', () => { test('when option after argument and _parseOptionsFollowingArguments(false) then option not parsed', () => { const program = new commander.Command(); - program._parseOptionsFollowingArguments = false; + program._allowOptionsFollowingArguments = false; program .option('-d, --debug') .arguments('[arg]'); @@ -37,7 +37,7 @@ test('when global option after subcommand then global option parsed', () => { test('when global option after subcommand and parseGlobalOptionsAnywhere(false) then global option not parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); - program._parseGlobalOptionsAnywhere = false; + program._allowGlobalOptionsAnywhere = false; program .option('-d, --debug'); const sub = program.command('sub') @@ -52,7 +52,7 @@ test('when global option after subcommand and parseGlobalOptionsAnywhere(false) test('when option after subcommand is global and local and parseGlobalOptionsAnywhere(false) then option parsed as local', () => { const mockAction = jest.fn(); const program = new commander.Command(); - program._parseGlobalOptionsAnywhere = false; + program._allowGlobalOptionsAnywhere = false; program .option('-d, --debug'); const sub = program.command('sub') From 304ad64247ee07763dd12776dcee44753df49e7d Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 14:19:24 +1300 Subject: [PATCH 06/26] Another try at naming --- index.js | 8 ++++---- tests/command.optionsBeforeArguments.test.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index a303af0da..35643aba9 100644 --- a/index.js +++ b/index.js @@ -552,8 +552,8 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; - this._allowGlobalOptionsAnywhere = true; // before and after subcommands - this._allowOptionsFollowingArguments = true; + this._allowGlobalOptionsAnywhere = true; // in particular, including after subcommands + this._passThroughOptions = false; // see .configureOutput() for docs this._outputConfiguration = { @@ -1619,8 +1619,8 @@ class Command extends EventEmitter { unknown.push(...args); break; } - // check whether to stop parsing options because hit argument - if (!this._allowOptionsFollowingArguments && !this._findCommand(arg)) { + // check whether to stop parsing options because hit command-argument + if (this._passThroughOptions && !this._findCommand(arg)) { operands.push(arg, ...args); break; } diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 271003da9..584da0584 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -10,9 +10,9 @@ test('when option after argument then option parsed', () => { expect(program.args).toEqual(['arg']); }); -test('when option after argument and _parseOptionsFollowingArguments(false) then option not parsed', () => { +test('when option after argument and passThroughOptions=true then option not parsed', () => { const program = new commander.Command(); - program._allowOptionsFollowingArguments = false; + program._passThroughOptions = true; program .option('-d, --debug') .arguments('[arg]'); @@ -34,7 +34,7 @@ test('when global option after subcommand then global option parsed', () => { expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); }); -test('when global option after subcommand and parseGlobalOptionsAnywhere(false) then global option not parsed', () => { +test('when global option after subcommand and allowGlobalOptionsAnywhere=false then global option not parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); program._allowGlobalOptionsAnywhere = false; @@ -49,7 +49,7 @@ test('when global option after subcommand and parseGlobalOptionsAnywhere(false) expect(mockAction).toBeCalledWith('--debug', sub.opts(), sub); }); -test('when option after subcommand is global and local and parseGlobalOptionsAnywhere(false) then option parsed as local', () => { +test('when option after subcommand is global and local and allowGlobalOptionsAnywhere=false then option parsed as local', () => { const mockAction = jest.fn(); const program = new commander.Command(); program._allowGlobalOptionsAnywhere = false; From ee98c4466900e8e8ac26214090c87f2177d5bdc0 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 18:57:33 +1300 Subject: [PATCH 07/26] Exclude options from special processing, which fixes help --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 35643aba9..4e20735dd 100644 --- a/index.js +++ b/index.js @@ -1612,7 +1612,7 @@ class Command extends EventEmitter { } // found first non-option - if (operands.length === 0 && unknown.length === 0) { + if (operands.length === 0 && unknown.length === 0 && !maybeOption(arg)) { // check whether to stop parsing global options because hit subcommand if (!this._allowGlobalOptionsAnywhere && this._findCommand(arg)) { operands.push(arg); From e155f3ac4d6af2e7c21b3e5c2b8849520abd383f Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 28 Dec 2020 20:29:04 +1300 Subject: [PATCH 08/26] Add help checks for new option configuration --- tests/command.optionsBeforeArguments.test.js | 33 +++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 584da0584..1bda411ad 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -63,7 +63,32 @@ test('when option after subcommand is global and local and allowGlobalOptionsAny expect(sub.opts().debug).toBe(true); }); -// arg --help -// sub --help -// help sub -// default command, arg +describe('program with _allowGlobalOptionsAnywhere=false and subcommand with _passThroughOptions=true', () => { + test.each([ + [[], true], + [['help'], true], + [['--help'], true], + [['sub'], false], + [['sub', '--help'], true], + [['sub', 'foo', '--help'], false] + ])('when user args %p then help called is %p', (userArgs, expectHelpCalled) => { + const program = new commander.Command(); + program._allowGlobalOptionsAnywhere = false; + program + .exitOverride() + .configureHelp({ formatHelp: () => '' }); + const sub = program.command('sub') + .exitOverride() + .configureHelp({ formatHelp: () => '' }) + .action(() => { }); + sub._passThroughOptions = true; + + let helpCalled = false; + try { + program.parse(userArgs, { from: 'user' }); + } catch (err) { + helpCalled = err.code === 'commander.helpDisplayed' || err.code === 'commander.help'; + } + expect(helpCalled).toEqual(expectHelpCalled); + }); +}); From fe29f6ea697dc46ab4e424f85f4789f6f3420817 Mon Sep 17 00:00:00 2001 From: John Gee Date: Tue, 29 Dec 2020 22:19:58 +1300 Subject: [PATCH 09/26] Rename after discovering needed for any positional options --- index.js | 4 ++-- tests/command.optionsBeforeArguments.test.js | 25 +++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 4e20735dd..4b68219be 100644 --- a/index.js +++ b/index.js @@ -552,7 +552,7 @@ class Command extends EventEmitter { this._combineFlagAndOptionalValue = true; this._description = ''; this._argsDescription = undefined; - this._allowGlobalOptionsAnywhere = true; // in particular, including after subcommands + this._enablePositionalOptions = false; this._passThroughOptions = false; // see .configureOutput() for docs @@ -1614,7 +1614,7 @@ class Command extends EventEmitter { // found first non-option if (operands.length === 0 && unknown.length === 0 && !maybeOption(arg)) { // check whether to stop parsing global options because hit subcommand - if (!this._allowGlobalOptionsAnywhere && this._findCommand(arg)) { + if (this._enablePositionalOptions && this._findCommand(arg)) { operands.push(arg); unknown.push(...args); break; diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 1bda411ad..8f1ea7dcb 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -21,6 +21,19 @@ test('when option after argument and passThroughOptions=true then option not par expect(program.args).toEqual(['arg', '--debug']); }); +// This shows bug, parsing of the "known" at the top level ate the argument before parsing by the subcommand. +// Could require that global parsing is off, to get pass-through? +// test('BUG: when option after subcommand argument and passThroughOptions=true then option not parsed', () => { +// const program = new commander.Command(); +// const sub = program.command('sub') +// .option('-d, --debug') +// .arguments(''); +// sub._passThroughOptions = true; +// program.parse(['sub', 'arg', '--debug'], { from: 'user' }); +// expect(sub.opts().debug).toBeUndefined(); +// expect(sub.args).toEqual(['arg', '--debug']); +// }); + test('when global option after subcommand then global option parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); @@ -34,10 +47,10 @@ test('when global option after subcommand then global option parsed', () => { expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); }); -test('when global option after subcommand and allowGlobalOptionsAnywhere=false then global option not parsed', () => { +test('when global option after subcommand and _enablePositionalOptions=true then global option not parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); - program._allowGlobalOptionsAnywhere = false; + program._enablePositionalOptions = true; program .option('-d, --debug'); const sub = program.command('sub') @@ -49,10 +62,10 @@ test('when global option after subcommand and allowGlobalOptionsAnywhere=false t expect(mockAction).toBeCalledWith('--debug', sub.opts(), sub); }); -test('when option after subcommand is global and local and allowGlobalOptionsAnywhere=false then option parsed as local', () => { +test('when option after subcommand is global and local and _enablePositionalOptions=true then option parsed as local', () => { const mockAction = jest.fn(); const program = new commander.Command(); - program._allowGlobalOptionsAnywhere = false; + program._enablePositionalOptions = true; program .option('-d, --debug'); const sub = program.command('sub') @@ -63,7 +76,7 @@ test('when option after subcommand is global and local and allowGlobalOptionsAny expect(sub.opts().debug).toBe(true); }); -describe('program with _allowGlobalOptionsAnywhere=false and subcommand with _passThroughOptions=true', () => { +describe('program with _enablePositionalOptions=true and subcommand with _passThroughOptions=true', () => { test.each([ [[], true], [['help'], true], @@ -73,7 +86,7 @@ describe('program with _allowGlobalOptionsAnywhere=false and subcommand with _pa [['sub', 'foo', '--help'], false] ])('when user args %p then help called is %p', (userArgs, expectHelpCalled) => { const program = new commander.Command(); - program._allowGlobalOptionsAnywhere = false; + program._enablePositionalOptions = true; program .exitOverride() .configureHelp({ formatHelp: () => '' }); From d781d77fc53e667c8785ed4f8d44bdf6e8bd9a4b Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 30 Dec 2020 12:38:01 +1300 Subject: [PATCH 10/26] Rework logic to hopefully cope with default commands. --- index.js | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/index.js b/index.js index 4b68219be..bd4dc13fa 100644 --- a/index.js +++ b/index.js @@ -1611,24 +1611,30 @@ class Command extends EventEmitter { } } - // found first non-option - if (operands.length === 0 && unknown.length === 0 && !maybeOption(arg)) { - // check whether to stop parsing global options because hit subcommand - if (this._enablePositionalOptions && this._findCommand(arg)) { - operands.push(arg); + // Not a recognised option by this command. + // Might be a command-argument, or subcommand option, or unknown option, or help command or option. + + // An unknown option means further arguments also classified as unknown so can be reprocessed by subcommands. + if (maybeOption(arg)) { + dest = unknown; + } + + // If using positionalOptions, stop processing our options at subcommand. + if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { + if (this._findCommand(arg)) { + dest.push(arg); unknown.push(...args); break; - } - // check whether to stop parsing options because hit command-argument - if (this._passThroughOptions && !this._findCommand(arg)) { - operands.push(arg, ...args); - break; + } else if (this._defaultCommandName) { + dest.push(arg); + unknown.push(...args); } } - // looks like an option but unknown, unknowns from here - if (arg.length > 1 && arg[0] === '-') { - dest = unknown; + // If using passThroughOptions, stop processing options at first command-argument. + if (this._passThroughOptions) { + dest.push(arg, ...args); + break; } // add arg From eef4d528d374207bb33e4c7d20a6931f1deeaaa9 Mon Sep 17 00:00:00 2001 From: John Gee Date: Wed, 30 Dec 2020 12:38:24 +1300 Subject: [PATCH 11/26] Expand basic tests. Positional options are tricky! --- tests/command.optionsBeforeArguments.test.js | 101 +++++++++++++------ 1 file changed, 70 insertions(+), 31 deletions(-) diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 8f1ea7dcb..3efd55c93 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -1,38 +1,70 @@ const commander = require('../'); -test('when option after argument then option parsed', () => { - const program = new commander.Command(); - program - .option('-d, --debug') - .arguments('[arg]'); - program.parse(['arg', '--debug'], { from: 'user' }); - expect(program.opts().debug).toBe(true); - expect(program.args).toEqual(['arg']); -}); +describe('program with _passThroughOptions=true', () => { + function makeProgram() { + const program = new commander.Command(); + program._passThroughOptions = true; + program + .option('-d, --debug') + .arguments(''); + return program; + } -test('when option after argument and passThroughOptions=true then option not parsed', () => { - const program = new commander.Command(); - program._passThroughOptions = true; - program - .option('-d, --debug') - .arguments('[arg]'); - program.parse(['arg', '--debug'], { from: 'user' }); - expect(program.opts().debug).toBeUndefined(); - expect(program.args).toEqual(['arg', '--debug']); -}); + test('when option before command-argument then option parsed', () => { + const program = makeProgram(); + program.parse(['--debug', 'arg'], { from: 'user' }); + expect(program.args).toEqual(['arg']); + expect(program.opts().debug).toBe(true); + }); + + test('when known option after command-argument then option passed through', () => { + const program = makeProgram(); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--debug']); + expect(program.opts().debug).toBeUndefined(); + }); + + test('when unknown option after command-argument then option passed through', () => { + const program = makeProgram(); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--pass']); + }); -// This shows bug, parsing of the "known" at the top level ate the argument before parsing by the subcommand. -// Could require that global parsing is off, to get pass-through? -// test('BUG: when option after subcommand argument and passThroughOptions=true then option not parsed', () => { -// const program = new commander.Command(); -// const sub = program.command('sub') -// .option('-d, --debug') -// .arguments(''); -// sub._passThroughOptions = true; -// program.parse(['sub', 'arg', '--debug'], { from: 'user' }); -// expect(sub.opts().debug).toBeUndefined(); -// expect(sub.args).toEqual(['arg', '--debug']); -// }); + test('when action handler and unknown option after command-argument then option passed through', () => { + const program = makeProgram(); + const mockAction = jest.fn(); + program.action(mockAction); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(mockAction).toHaveBeenCalledWith(['arg', '--pass'], program.opts(), program); + }); + + test('when help option (without command-argument) then help called', () => { + const program = makeProgram(); + const mockHelp = jest.fn(() => ''); + + program + .exitOverride() + .configureHelp({ formatHelp: mockHelp }); + try { + program.parse(['--help'], { from: 'user' }); + } catch (err) { + } + expect(mockHelp).toHaveBeenCalled(); + }); + + test('when help option after command-argument then option passed through', () => { + const program = makeProgram(); + program.parse(['arg', '--help'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--help']); + }); + + test('when version option after command-argument then option passed through', () => { + const program = makeProgram(); + program.version('1.2.3'); + program.parse(['arg', '--version'], { from: 'user' }); + expect(program.args).toEqual(['arg', '--version']); + }); +}); test('when global option after subcommand then global option parsed', () => { const mockAction = jest.fn(); @@ -85,6 +117,7 @@ describe('program with _enablePositionalOptions=true and subcommand with _passTh [['sub', '--help'], true], [['sub', 'foo', '--help'], false] ])('when user args %p then help called is %p', (userArgs, expectHelpCalled) => { + // also check which command calls help ???? const program = new commander.Command(); program._enablePositionalOptions = true; program @@ -105,3 +138,9 @@ describe('program with _enablePositionalOptions=true and subcommand with _passTh expect(helpCalled).toEqual(expectHelpCalled); }); }); + +// test for "foo sub" where sub is a parameter and not a command ???? + +// default command tests, including help + +// Interaction of unknowns with passThrough. From f20aff0e1905fa0086447abbc56e628e43e39d8f Mon Sep 17 00:00:00 2001 From: John Gee Date: Thu, 31 Dec 2020 08:51:33 +1300 Subject: [PATCH 12/26] Add first default command tests --- tests/command.optionsBeforeArguments.test.js | 153 ++++++++++++++++++- 1 file changed, 149 insertions(+), 4 deletions(-) diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 3efd55c93..86b9fa4b7 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -1,11 +1,14 @@ const commander = require('../'); -describe('program with _passThroughOptions=true', () => { +// The changes to parsing for positional options are subtle, and took extra care to work with +// implicit help and default commands. Lots of tests. + +describe('program with passThrough', () => { function makeProgram() { const program = new commander.Command(); program._passThroughOptions = true; program - .option('-d, --debug') + .option('-p, --debug') .arguments(''); return program; } @@ -66,6 +69,146 @@ describe('program with _passThroughOptions=true', () => { }); }); +// ----------------------------------------------------------- + +describe('program with positionalOptions and subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program._enablePositionalOptions = true; + program + .option('-s, --shared') + .arguments(''); + const sub = program.command('sub') + .arguments('[arg]') + .option('-s, --shared') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + return { program, sub }; + } + + test('when global option before subcommand then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['--shared', 'sub'], { from: 'user' }); + expect(program.opts().shared).toBe(true); + }); + + test('when shared option after subcommand then parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '--shared'], { from: 'user' }); + expect(sub.opts().shared).toBe(true); + expect(program.opts().shared).toBeUndefined(); + }); + + test('when shared option after subcommand argument then parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--shared'], { from: 'user' }); + expect(sub.opts().shared).toBe(true); + expect(sub.args).toEqual(['arg']); + expect(program.opts().shared).toBeUndefined(); + }); + + test.each([ + [[], 1, 0], + [['sub'], 0, 0], + [['--help'], 1, 0], + [['sub', '--help'], 0, 1], + [['sub', 'foo', '--help'], 0, 1], + [['help'], 1, 0], + [['help', 'sub'], 0, 1] + ])('help: when user args %p then program/sub help called %p/%p', (userArgs, expectProgramHelpCount, expectSubHelpCount) => { + const { program, sub } = makeProgram(); + const mockProgramHelp = jest.fn(); + program + .exitOverride() + .configureHelp({ formatHelp: mockProgramHelp }); + const mockSubHelp = jest.fn(); + sub + .exitOverride() + .configureHelp({ formatHelp: mockSubHelp }); + + try { + program.parse(userArgs, { from: 'user' }); + } catch (err) { + } + expect(mockProgramHelp).toHaveBeenCalledTimes(expectProgramHelpCount); + expect(mockSubHelp).toHaveBeenCalledTimes(expectSubHelpCount); + }); +}); + +// --------------------------------------------------------------- + +describe('program with positionalOptions and default subcommand called sub', () => { + function makeProgram() { + const program = new commander.Command(); + program._enablePositionalOptions = true; + program + .option('-s, --shared') + .option('-g, --global') + .arguments(''); + const sub = program.command('sub', { isDefault: true }) + .arguments('[args...]') + .option('-s, --shared') + .option('-d, --default') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + program.command('another'); // Not used, but normal to have more than one subcommand if have a default. + return { program, sub }; + } + + test('when program option before sub option then program option read by program', () => { + const { program } = makeProgram(); + program.parse(['--global', '--default'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when global option before sub option then sub option read by sub', () => { + const { program, sub } = makeProgram(); + program.parse(['--global', '--default'], { from: 'user' }); + expect(sub.opts().default).toBe(true); + }); + + test('when shared option before sub argument then option read by program', () => { + const { program } = makeProgram(); + program.parse(['--shared', 'foo'], { from: 'user' }); + expect(program.opts().shared).toBe(true); + }); + + test('when shared option after sub argument then option read by sub', () => { + const { program, sub } = makeProgram(); + program.parse(['foo', '--shared'], { from: 'user' }); + expect(sub.opts().shared).toBe(true); + }); + + test.each([ + [[], 0, 0], + [['--help'], 1, 0], + [['help'], 1, 0] + ])('help: when user args %p then program/sub help called %p/%p', (userArgs, expectProgramHelpCount, expectSubHelpCount) => { + const { program, sub } = makeProgram(); + const mockProgramHelp = jest.fn(); + program + .exitOverride() + .configureHelp({ formatHelp: mockProgramHelp }); + const mockSubHelp = jest.fn(); + sub + .exitOverride() + .configureHelp({ formatHelp: mockSubHelp }); + + try { + program.parse(userArgs, { from: 'user' }); + } catch (err) { + } + expect(mockProgramHelp).toHaveBeenCalledTimes(expectProgramHelpCount); + expect(mockSubHelp).toHaveBeenCalledTimes(expectSubHelpCount); + }); +}); + +// --------------------------------------------------------------------------- +// WIP from here +// --------------------------------------------------------------------------- + +// --------- subcommand passThrough + +// --------- subcommand passThrough with default ???? + test('when global option after subcommand then global option parsed', () => { const mockAction = jest.fn(); const program = new commander.Command(); @@ -141,6 +284,8 @@ describe('program with _enablePositionalOptions=true and subcommand with _passTh // test for "foo sub" where sub is a parameter and not a command ???? -// default command tests, including help +// Interaction of unknowns with passThrough ???? + +// Test action handler at least once in each block ???? -// Interaction of unknowns with passThrough. +// Test default command support does not break any other cases ???? From 91468d3493e96fa56176f030cea9ebc19113cfae Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 1 Jan 2021 12:55:28 +1300 Subject: [PATCH 13/26] Fill out more tests --- tests/command.optionsBeforeArguments.test.js | 172 +++++++++++-------- 1 file changed, 103 insertions(+), 69 deletions(-) diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 86b9fa4b7..938447ee3 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -106,6 +106,13 @@ describe('program with positionalOptions and subcommand', () => { expect(program.opts().shared).toBeUndefined(); }); + test('when shared option before and after subcommand then both parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['--shared', 'sub', '--shared'], { from: 'user' }); + expect(program.opts().shared).toBe(true); + expect(sub.opts().shared).toBe(true); + }); + test.each([ [[], 1, 0], [['sub'], 0, 0], @@ -136,7 +143,7 @@ describe('program with positionalOptions and subcommand', () => { // --------------------------------------------------------------- -describe('program with positionalOptions and default subcommand called sub', () => { +describe('program with positionalOptions and default subcommand (called sub)', () => { function makeProgram() { const program = new commander.Command(); program._enablePositionalOptions = true; @@ -159,7 +166,7 @@ describe('program with positionalOptions and default subcommand called sub', () expect(program.opts().global).toBe(true); }); - test('when global option before sub option then sub option read by sub', () => { + test('when program option before sub option then sub option read by sub', () => { const { program, sub } = makeProgram(); program.parse(['--global', '--default'], { from: 'user' }); expect(sub.opts().default).toBe(true); @@ -201,88 +208,115 @@ describe('program with positionalOptions and default subcommand called sub', () }); }); -// --------------------------------------------------------------------------- -// WIP from here -// --------------------------------------------------------------------------- +// ------------------------------------------------------------------------------ -// --------- subcommand passThrough +describe('subcommand with passThrough', () => { + function makeProgram() { + const program = new commander.Command(); + program._enablePositionalOptions = true; + program + .option('-s, --shared') + .arguments(''); + const sub = program.command('sub') + .arguments('[args...]') + .option('-s, --shared') + .option('-d, --debug') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + sub._passThroughOptions = true; + return { program, sub }; + } -// --------- subcommand passThrough with default ???? + test('when option before command-argument then option parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '--debug', 'arg'], { from: 'user' }); + expect(sub.args).toEqual(['arg']); + expect(sub.opts().debug).toBe(true); + }); -test('when global option after subcommand then global option parsed', () => { - const mockAction = jest.fn(); - const program = new commander.Command(); - program - .option('-d, --debug'); - const sub = program.command('sub') - .arguments('[arg]') - .action(mockAction); - program.parse(['sub', '--debug', 'arg'], { from: 'user' }); - expect(program.opts().debug).toBe(true); - expect(mockAction).toBeCalledWith('arg', sub.opts(), sub); -}); + test('when known option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--debug'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--debug']); + expect(sub.opts().debug).toBeUndefined(); + }); -test('when global option after subcommand and _enablePositionalOptions=true then global option not parsed', () => { - const mockAction = jest.fn(); - const program = new commander.Command(); - program._enablePositionalOptions = true; - program - .option('-d, --debug'); - const sub = program.command('sub') - .arguments('[arg]') - .allowUnknownOption() - .action(mockAction); - program.parse(['sub', '--debug'], { from: 'user' }); - expect(program.opts().debug).toBeUndefined(); - expect(mockAction).toBeCalledWith('--debug', sub.opts(), sub); -}); + test('when unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--pass'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--pass']); + }); + + test('when action handler and unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + const mockAction = jest.fn(); + sub.action(mockAction); + program.parse(['sub', 'arg', '--pass'], { from: 'user' }); + expect(mockAction).toHaveBeenCalledWith(['arg', '--pass'], sub.opts(), sub); + }); + + test('when help option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', 'arg', '--help'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--help']); + }); -test('when option after subcommand is global and local and _enablePositionalOptions=true then option parsed as local', () => { - const mockAction = jest.fn(); - const program = new commander.Command(); - program._enablePositionalOptions = true; - program - .option('-d, --debug'); - const sub = program.command('sub') - .option('-d, --debug') - .action(mockAction); - program.parse(['sub', '--debug'], { from: 'user' }); - expect(program.opts().debug).toBeUndefined(); - expect(sub.opts().debug).toBe(true); + test('when version option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.version('1.2.3'); + program.parse(['sub', 'arg', '--version'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--version']); + }); + + test('when shared option before sub and after sub and after sub parameter then all three parsed', () => { + const { program, sub } = makeProgram(); + program.version('1.2.3'); + program.parse(['--shared', 'sub', '--shared', 'arg', '--shared'], { from: 'user' }); + expect(program.opts().shared).toBe(true); + expect(sub.opts().shared).toBe(true); + expect(sub.args).toEqual(['arg', '--shared']); + }); }); -describe('program with _enablePositionalOptions=true and subcommand with _passThroughOptions=true', () => { - test.each([ - [[], true], - [['help'], true], - [['--help'], true], - [['sub'], false], - [['sub', '--help'], true], - [['sub', 'foo', '--help'], false] - ])('when user args %p then help called is %p', (userArgs, expectHelpCalled) => { - // also check which command calls help ???? +// ------------------------------------------------------------------------------ + +describe('program with action handler and positionalOptions and subcommand', () => { + function makeProgram() { const program = new commander.Command(); program._enablePositionalOptions = true; program - .exitOverride() - .configureHelp({ formatHelp: () => '' }); + .option('-g, --global') + .arguments('') + .action(() => {}); const sub = program.command('sub') - .exitOverride() - .configureHelp({ formatHelp: () => '' }) - .action(() => { }); - sub._passThroughOptions = true; + .arguments('[arg]') + .action(() => {}); + return { program, sub }; + } - let helpCalled = false; - try { - program.parse(userArgs, { from: 'user' }); - } catch (err) { - helpCalled = err.code === 'commander.helpDisplayed' || err.code === 'commander.help'; - } - expect(helpCalled).toEqual(expectHelpCalled); + test('when global option before parameter then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['--global', 'foo'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when global option after parameter then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['foo', '--global'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); + + test('when global option after parameter with same name as subcommand then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['foo', 'sub', '--global'], { from: 'user' }); + expect(program.opts().global).toBe(true); }); }); -// test for "foo sub" where sub is a parameter and not a command ???? +// --------------------------------------------------------------------------- +// WIP from here +// --------------------------------------------------------------------------- + +// --------- subcommand passThrough with default ???? // Interaction of unknowns with passThrough ???? From 3b6cd3fa6584614d689a67554466554359ea7ed8 Mon Sep 17 00:00:00 2001 From: John Gee Date: Fri, 1 Jan 2021 15:41:36 +1300 Subject: [PATCH 14/26] Add setters, and throw when passThrough without enabling positional --- index.js | 29 ++++++++++++++++++++ tests/command.chain.test.js | 12 ++++++++ tests/command.optionsBeforeArguments.test.js | 12 ++++---- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index bd4dc13fa..f5dd544f4 100644 --- a/index.js +++ b/index.js @@ -1135,6 +1135,35 @@ class Command extends EventEmitter { return this; }; + /** + * Enable positional options. Positional means global options are specified before subcommands which lets + * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions. + * The default behaviour is non-positional and global options may appear anywhere on the command line. + * + * @param {Boolean} [positional=true] + */ + enablePositionalOptions(positional = true) { + this._enablePositionalOptions = !!positional; + return this; + }; + + /** + * Pass through options that come after command-arguments rather than treat them as command-options, + * so actual command-options come before command-arguments. Turning this on for a subcommand requires + * positional options to have been enabled on the program (parent commands). + * The default behaviour is non-positional and options may appear before or after command-arguments. + * + * @param {Boolean} [passThrough=true] + * for unknown options. + */ + passThroughOptions(passThrough = true) { + this._passThroughOptions = !!passThrough; + if (!!this.parent && !this.parent._enablePositionalOptions) { + throw new Error('passThroughOptions() can not be used because enablePositionOptions() has been called on program (parent commands)'); + } + return this; + }; + /** * Whether to store option values as properties on command object, * or store separately (specify false). In both cases the option values can be accessed using .opts(). diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index a53fdbb84..4987d8c8b 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -141,4 +141,16 @@ describe('Command methods that should return this for chaining', () => { const result = program.configureOutput({ }); expect(result).toBe(program); }); + + test('when call .passThroughOptions() then returns this', () => { + const program = new Command(); + const result = program.passThroughOptions(); + expect(result).toBe(program); + }); + + test('when call .enablePositionOptions() then returns this', () => { + const program = new Command(); + const result = program.enablePositionalOptions(); + expect(result).toBe(program); + }); }); diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.optionsBeforeArguments.test.js index 938447ee3..828998e00 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.optionsBeforeArguments.test.js @@ -6,7 +6,7 @@ const commander = require('../'); describe('program with passThrough', () => { function makeProgram() { const program = new commander.Command(); - program._passThroughOptions = true; + program.passThroughOptions(); program .option('-p, --debug') .arguments(''); @@ -74,7 +74,7 @@ describe('program with passThrough', () => { describe('program with positionalOptions and subcommand', () => { function makeProgram() { const program = new commander.Command(); - program._enablePositionalOptions = true; + program.enablePositionalOptions(); program .option('-s, --shared') .arguments(''); @@ -146,7 +146,7 @@ describe('program with positionalOptions and subcommand', () => { describe('program with positionalOptions and default subcommand (called sub)', () => { function makeProgram() { const program = new commander.Command(); - program._enablePositionalOptions = true; + program.enablePositionalOptions(); program .option('-s, --shared') .option('-g, --global') @@ -213,7 +213,7 @@ describe('program with positionalOptions and default subcommand (called sub)', ( describe('subcommand with passThrough', () => { function makeProgram() { const program = new commander.Command(); - program._enablePositionalOptions = true; + program.enablePositionalOptions(); program .option('-s, --shared') .arguments(''); @@ -222,7 +222,7 @@ describe('subcommand with passThrough', () => { .option('-s, --shared') .option('-d, --debug') .action(() => {}); // Not used, but normal to have action handler on subcommand. - sub._passThroughOptions = true; + sub.passThroughOptions(); return { program, sub }; } @@ -282,7 +282,7 @@ describe('subcommand with passThrough', () => { describe('program with action handler and positionalOptions and subcommand', () => { function makeProgram() { const program = new commander.Command(); - program._enablePositionalOptions = true; + program.enablePositionalOptions(); program .option('-g, --global') .arguments('') From bb95593994ad1abdf0361c6eea6528b1b3417c83 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 12:03:35 +1300 Subject: [PATCH 15/26] Rename test file --- ...eforeArguments.test.js => command.positionalOptions.test.js} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{command.optionsBeforeArguments.test.js => command.positionalOptions.test.js} (99%) diff --git a/tests/command.optionsBeforeArguments.test.js b/tests/command.positionalOptions.test.js similarity index 99% rename from tests/command.optionsBeforeArguments.test.js rename to tests/command.positionalOptions.test.js index 828998e00..1087f3d96 100644 --- a/tests/command.optionsBeforeArguments.test.js +++ b/tests/command.positionalOptions.test.js @@ -322,4 +322,4 @@ describe('program with action handler and positionalOptions and subcommand', () // Test action handler at least once in each block ???? -// Test default command support does not break any other cases ???? +// Test error when passThrough in subcommand without positionalOptions in parent From 9c5b77a902d2ac0dce04c1a9533e6397e37509ba Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 12:09:28 +1300 Subject: [PATCH 16/26] Add TypeScript --- typings/commander-tests.ts | 8 ++++++++ typings/index.d.ts | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/typings/commander-tests.ts b/typings/commander-tests.ts index 607402dd3..f54a3e1cf 100644 --- a/typings/commander-tests.ts +++ b/typings/commander-tests.ts @@ -147,6 +147,14 @@ const allowUnknownOptionThis2: commander.Command = program.allowUnknownOption(fa const allowExcessArgumentsThis1: commander.Command = program.allowExcessArguments(); const allowExcessArgumentsThis2: commander.Command = program.allowExcessArguments(false); +// enablePositionalOptions +const enablePositionalOptionsThis1: commander.Command = program.enablePositionalOptions(); +const enablePositionalOptionsThis2: commander.Command = program.enablePositionalOptions(false); + +// passThroughOptions +const passThroughOptionsThis1: commander.Command = program.passThroughOptions(); +const passThroughOptionsThis2: commander.Command = program.passThroughOptions(false); + // parse const parseThis1: commander.Command = program.parse(); const parseThis2: commander.Command = program.parse(process.argv); diff --git a/typings/index.d.ts b/typings/index.d.ts index 906a05731..c6fe842af 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -400,6 +400,27 @@ declare namespace commander { */ allowExcessArguments(allowExcess?: boolean): this; + /** + * Enable positional options. Positional means global options are specified before subcommands which lets + * subcommands reuse the same option names, and also enables subcommands to turn on passThroughOptions. + * + * The default behaviour is non-positional and global options may appear anywhere on the command line. + * + * @returns `this` command for chaining + */ + enablePositionalOptions(positional?: boolean): this; + + /** + * Pass through options that come after command-arguments rather than treat them as command-options, + * so actual command-options come before command-arguments. Turning this on for a subcommand requires + * positional options to have been enabled on the program (parent commands). + * + * The default behaviour is non-positional and options may appear before or after command-arguments. + * + * @returns `this` command for chaining + */ + passThroughOptions(passThrough?: boolean): this; + /** * Parse `argv`, setting options and invoking commands when defined. * From 6a4524b64cdabe354f114ff80ce5497890988d5c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 14:46:27 +1300 Subject: [PATCH 17/26] Add tests. Fix help handling by making explicit. --- index.js | 22 ++++-- tests/command.positionalOptions.test.js | 100 ++++++++++++++++++------ 2 files changed, 90 insertions(+), 32 deletions(-) diff --git a/index.js b/index.js index f5dd544f4..58ebf70d9 100644 --- a/index.js +++ b/index.js @@ -1158,8 +1158,8 @@ class Command extends EventEmitter { */ passThroughOptions(passThrough = true) { this._passThroughOptions = !!passThrough; - if (!!this.parent && !this.parent._enablePositionalOptions) { - throw new Error('passThroughOptions() can not be used because enablePositionOptions() has been called on program (parent commands)'); + if (!!this.parent && passThrough && !this.parent._enablePositionalOptions) { + throw new Error('passThroughOptions can not be used without turning on enablePositionOptions for parent command(s)'); } return this; }; @@ -1650,19 +1650,25 @@ class Command extends EventEmitter { // If using positionalOptions, stop processing our options at subcommand. if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { - if (this._findCommand(arg)) { - dest.push(arg); - unknown.push(...args); + if (this._hasImplicitHelpCommand() && arg === this._helpCommandName) { + operands.push(arg); + if (args.length > 0) operands.push(...args); + break; + } if (this._findCommand(arg)) { + operands.push(arg); + if (args.length > 0) unknown.push(...args); break; } else if (this._defaultCommandName) { - dest.push(arg); - unknown.push(...args); + unknown.push(arg); + if (args.length > 0) unknown.push(...args); + break; } } // If using passThroughOptions, stop processing options at first command-argument. if (this._passThroughOptions) { - dest.push(arg, ...args); + dest.push(arg); + if (args.length > 0) dest.push(...args); break; } diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js index 1087f3d96..43466688a 100644 --- a/tests/command.positionalOptions.test.js +++ b/tests/command.positionalOptions.test.js @@ -74,43 +74,43 @@ describe('program with passThrough', () => { describe('program with positionalOptions and subcommand', () => { function makeProgram() { const program = new commander.Command(); - program.enablePositionalOptions(); program - .option('-s, --shared') + .enablePositionalOptions() + .option('-s, --shared ') .arguments(''); const sub = program.command('sub') .arguments('[arg]') - .option('-s, --shared') + .option('-s, --shared ') .action(() => {}); // Not used, but normal to have action handler on subcommand. return { program, sub }; } test('when global option before subcommand then global option parsed', () => { const { program } = makeProgram(); - program.parse(['--shared', 'sub'], { from: 'user' }); - expect(program.opts().shared).toBe(true); + program.parse(['--shared', 'program', 'sub'], { from: 'user' }); + expect(program.opts().shared).toEqual('program'); }); test('when shared option after subcommand then parsed by subcommand', () => { const { program, sub } = makeProgram(); - program.parse(['sub', '--shared'], { from: 'user' }); - expect(sub.opts().shared).toBe(true); + program.parse(['sub', '--shared', 'local'], { from: 'user' }); + expect(sub.opts().shared).toEqual('local'); expect(program.opts().shared).toBeUndefined(); }); test('when shared option after subcommand argument then parsed by subcommand', () => { const { program, sub } = makeProgram(); - program.parse(['sub', 'arg', '--shared'], { from: 'user' }); - expect(sub.opts().shared).toBe(true); + program.parse(['sub', 'arg', '--shared', 'local'], { from: 'user' }); + expect(sub.opts().shared).toEqual('local'); expect(sub.args).toEqual(['arg']); expect(program.opts().shared).toBeUndefined(); }); test('when shared option before and after subcommand then both parsed', () => { const { program, sub } = makeProgram(); - program.parse(['--shared', 'sub', '--shared'], { from: 'user' }); - expect(program.opts().shared).toBe(true); - expect(sub.opts().shared).toBe(true); + program.parse(['--shared', 'program', 'sub', '--shared', 'local'], { from: 'user' }); + expect(program.opts().shared).toEqual('program'); + expect(sub.opts().shared).toEqual('local'); }); test.each([ @@ -146,8 +146,8 @@ describe('program with positionalOptions and subcommand', () => { describe('program with positionalOptions and default subcommand (called sub)', () => { function makeProgram() { const program = new commander.Command(); - program.enablePositionalOptions(); program + .enablePositionalOptions() .option('-s, --shared') .option('-g, --global') .arguments(''); @@ -213,16 +213,16 @@ describe('program with positionalOptions and default subcommand (called sub)', ( describe('subcommand with passThrough', () => { function makeProgram() { const program = new commander.Command(); - program.enablePositionalOptions(); program - .option('-s, --shared') + .enablePositionalOptions() + .option('-s, --shared ') .arguments(''); const sub = program.command('sub') + .passThroughOptions() .arguments('[args...]') - .option('-s, --shared') + .option('-s, --shared ') .option('-d, --debug') .action(() => {}); // Not used, but normal to have action handler on subcommand. - sub.passThroughOptions(); return { program, sub }; } @@ -269,21 +269,64 @@ describe('subcommand with passThrough', () => { test('when shared option before sub and after sub and after sub parameter then all three parsed', () => { const { program, sub } = makeProgram(); - program.version('1.2.3'); - program.parse(['--shared', 'sub', '--shared', 'arg', '--shared'], { from: 'user' }); - expect(program.opts().shared).toBe(true); - expect(sub.opts().shared).toBe(true); + program.parse(['--shared=global', 'sub', '--shared=local', 'arg', '--shared'], { from: 'user' }); + expect(program.opts().shared).toEqual('global'); + expect(sub.opts().shared).toEqual('local'); expect(sub.args).toEqual(['arg', '--shared']); }); }); // ------------------------------------------------------------------------------ +describe('default command with passThrough', () => { + function makeProgram() { + const program = new commander.Command(); + program + .enablePositionalOptions(); + const sub = program.command('sub', { isDefault: true }) + .passThroughOptions() + .arguments('[args...]') + .option('-d, --debug') + .action(() => {}); // Not used, but normal to have action handler on subcommand. + return { program, sub }; + } + + test('when option before command-argument then option parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['--debug', 'arg'], { from: 'user' }); + expect(sub.args).toEqual(['arg']); + expect(sub.opts().debug).toBe(true); + }); + + test('when known option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['arg', '--debug'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--debug']); + expect(sub.opts().debug).toBeUndefined(); + }); + + test('when unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(sub.args).toEqual(['arg', '--pass']); + }); + + test('when action handler and unknown option after command-argument then option passed through', () => { + const { program, sub } = makeProgram(); + const mockAction = jest.fn(); + sub.action(mockAction); + program.parse(['arg', '--pass'], { from: 'user' }); + expect(mockAction).toHaveBeenCalledWith(['arg', '--pass'], sub.opts(), sub); + }); +}); + +// ------------------------------------------------------------------------------ + describe('program with action handler and positionalOptions and subcommand', () => { function makeProgram() { const program = new commander.Command(); - program.enablePositionalOptions(); program + .enablePositionalOptions() .option('-g, --global') .arguments('') .action(() => {}); @@ -312,6 +355,17 @@ describe('program with action handler and positionalOptions and subcommand', () }); }); +// ------------------------------------------------------------------------------ + +test('when program not positional and turn on passthrough in subcommand then error', () => { + const program = new commander.Command(); + const sub = program.command('sub'); + + expect(() => { + sub.passThroughOptions(); + }).toThrow(); +}); + // --------------------------------------------------------------------------- // WIP from here // --------------------------------------------------------------------------- @@ -321,5 +375,3 @@ describe('program with action handler and positionalOptions and subcommand', () // Interaction of unknowns with passThrough ???? // Test action handler at least once in each block ???? - -// Test error when passThrough in subcommand without positionalOptions in parent From 4d26a151c027d9c47478b5e976bcbec633c15dad Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 16:34:40 +1300 Subject: [PATCH 18/26] Reorder tests --- index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index 58ebf70d9..004b87352 100644 --- a/index.js +++ b/index.js @@ -1650,13 +1650,13 @@ class Command extends EventEmitter { // If using positionalOptions, stop processing our options at subcommand. if ((this._enablePositionalOptions || this._passThroughOptions) && operands.length === 0 && unknown.length === 0) { - if (this._hasImplicitHelpCommand() && arg === this._helpCommandName) { + if (this._findCommand(arg)) { operands.push(arg); - if (args.length > 0) operands.push(...args); + if (args.length > 0) unknown.push(...args); break; - } if (this._findCommand(arg)) { + } else if (arg === this._helpCommandName && this._hasImplicitHelpCommand()) { operands.push(arg); - if (args.length > 0) unknown.push(...args); + if (args.length > 0) operands.push(...args); break; } else if (this._defaultCommandName) { unknown.push(arg); From 4136ce8327e46002852133f93e61aa8022b0338f Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 16:41:35 +1300 Subject: [PATCH 19/26] Use usual indentation --- tests/command.positionalOptions.test.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js index 43466688a..948105784 100644 --- a/tests/command.positionalOptions.test.js +++ b/tests/command.positionalOptions.test.js @@ -78,7 +78,8 @@ describe('program with positionalOptions and subcommand', () => { .enablePositionalOptions() .option('-s, --shared ') .arguments(''); - const sub = program.command('sub') + const sub = program + .command('sub') .arguments('[arg]') .option('-s, --shared ') .action(() => {}); // Not used, but normal to have action handler on subcommand. @@ -151,7 +152,8 @@ describe('program with positionalOptions and default subcommand (called sub)', ( .option('-s, --shared') .option('-g, --global') .arguments(''); - const sub = program.command('sub', { isDefault: true }) + const sub = program + .command('sub', { isDefault: true }) .arguments('[args...]') .option('-s, --shared') .option('-d, --default') @@ -217,7 +219,8 @@ describe('subcommand with passThrough', () => { .enablePositionalOptions() .option('-s, --shared ') .arguments(''); - const sub = program.command('sub') + const sub = program + .command('sub') .passThroughOptions() .arguments('[args...]') .option('-s, --shared ') @@ -283,7 +286,8 @@ describe('default command with passThrough', () => { const program = new commander.Command(); program .enablePositionalOptions(); - const sub = program.command('sub', { isDefault: true }) + const sub = program + .command('sub', { isDefault: true }) .passThroughOptions() .arguments('[args...]') .option('-d, --debug') @@ -330,7 +334,8 @@ describe('program with action handler and positionalOptions and subcommand', () .option('-g, --global') .arguments('') .action(() => {}); - const sub = program.command('sub') + const sub = program + .command('sub') .arguments('[arg]') .action(() => {}); return { program, sub }; From 17472b9fe0583b3c10600727dbaca2d1e250cec3 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 17:27:25 +1300 Subject: [PATCH 20/26] Make _enablePositionalOptions inherited to simpify nested commands --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 004b87352..c35f80a05 100644 --- a/index.js +++ b/index.js @@ -635,6 +635,7 @@ class Command extends EventEmitter { cmd._exitCallback = this._exitCallback; cmd._storeOptionsAsProperties = this._storeOptionsAsProperties; cmd._combineFlagAndOptionalValue = this._combineFlagAndOptionalValue; + cmd._enablePositionalOptions = this._enablePositionalOptions; cmd._executableFile = opts.executableFile || null; // Custom name for executable file, set missing to null to match constructor this.commands.push(cmd); From 2c9373ff18f9910ac7a2f223332d871a7903dd63 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 17:27:35 +1300 Subject: [PATCH 21/26] Add examples --- examples/pass-through-options.js | 23 +++++++++++++++++++++++ examples/positional-options.js | 27 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 examples/pass-through-options.js create mode 100644 examples/positional-options.js diff --git a/examples/pass-through-options.js b/examples/pass-through-options.js new file mode 100644 index 000000000..af5c8497c --- /dev/null +++ b/examples/pass-through-options.js @@ -0,0 +1,23 @@ +#!/usr/bin/env node + +// const { Command } = require('commander'); // (normal include) +const { Command } = require('../'); // include commander in git clone of commander repo +const program = new Command(); + +program + .arguments(' [args...]') + .passThroughOptions() + .option('-d, --dry-run') + .action((utility, args, options) => { + const action = options.dryRun ? 'Would run' : 'Running'; + console.log(`${action}: ${utility} ${args.join(' ')}`); + }); + +program.parse(); + +// Try the following: +// +// node pass-through-options.js git status +// node pass-through-options.js git --version +// node pass-through-options.js --dry-run git checkout -b new-branch +// node pass-through-options.js git push --dry-run diff --git a/examples/positional-options.js b/examples/positional-options.js new file mode 100644 index 000000000..32edd3918 --- /dev/null +++ b/examples/positional-options.js @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +// const { Command } = require('commander'); // (normal include) +const { Command } = require('../'); // include commander in git clone of commander repo +const program = new Command(); + +program + .enablePositionalOptions() + .option('-p, --progress'); + +program + .command('upload ') + .option('-p, --port ', 'port number', 80) + .action((file, options) => { + if (program.opts().progress) console.log('Starting upload...'); + console.log(`Uploading ${file} to port ${options.port}`); + if (program.opts().progress) console.log('Finished upload'); + }); + +program.parse(); + +// Try the following: +// +// node positional-options.js upload test.js +// node positional-options.js -p upload test.js +// node positional-options.js upload -p 8080 test.js +// node positional-options.js -p upload -p 8080 test.js From ca47d95c9409b13154d22894f8597bc4448ba630 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 18:13:52 +1300 Subject: [PATCH 22/26] Add tests for some less common setups --- tests/command.positionalOptions.test.js | 77 ++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js index 948105784..cace54d5c 100644 --- a/tests/command.positionalOptions.test.js +++ b/tests/command.positionalOptions.test.js @@ -8,7 +8,7 @@ describe('program with passThrough', () => { const program = new commander.Command(); program.passThroughOptions(); program - .option('-p, --debug') + .option('-d, --debug') .arguments(''); return program; } @@ -371,12 +371,75 @@ test('when program not positional and turn on passthrough in subcommand then err }).toThrow(); }); -// --------------------------------------------------------------------------- -// WIP from here -// --------------------------------------------------------------------------- +// ------------------------------------------------------------------------------ -// --------- subcommand passThrough with default ???? +describe('program with action handler and passThrough and subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program + .passThroughOptions() + .option('-g, --global') + .arguments('') + .action(() => {}); + const sub = program + .command('sub') + .arguments('[arg]') + .option('-g, --group') + .option('-d, --debug') + .action(() => {}); + return { program, sub }; + } + + test('when global option before parameter then global option parsed', () => { + const { program } = makeProgram(); + program.parse(['--global', 'foo'], { from: 'user' }); + expect(program.opts().global).toBe(true); + }); -// Interaction of unknowns with passThrough ???? + test('when global option after parameter then passed through', () => { + const { program } = makeProgram(); + program.parse(['foo', '--global'], { from: 'user' }); + expect(program.args).toEqual(['foo', '--global']); + }); -// Test action handler at least once in each block ???? + test('when subcommand option after subcommand then option parsed', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toBe(true); + }); + + // This is somewhat of a side-affect of supporting previous test. + test('when shared option after subcommand then parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.parse(['sub', '-g'], { from: 'user' }); + expect(sub.opts().group).toBe(true); + expect(program.opts().global).toBeUndefined(); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('program with allowUnknownOption', () => { + test('when passThroughOptions and unknown option then arguments from unknown passed through', () => { + const program = new commander.Command(); + program + .passThroughOptions() + .allowUnknownOption() + .option('--debug'); + + program.parse(['--unknown', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['--unknown', '--debug']); + }); + + test('when positionalOptions and unknown option then known options then known option parsed', () => { + const program = new commander.Command(); + program + .enablePositionalOptions() + .allowUnknownOption() + .option('--debug'); + + program.parse(['--unknown', '--debug'], { from: 'user' }); + expect(program.opts().debug).toBe(true); + expect(program.args).toEqual(['--unknown']); + }); +}); From a021878bdb5fdf5826082cc18fd9a16cabeb4fb5 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sat, 2 Jan 2021 18:37:58 +1300 Subject: [PATCH 23/26] Test the boring true/false parameters --- tests/command.positionalOptions.test.js | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/command.positionalOptions.test.js b/tests/command.positionalOptions.test.js index cace54d5c..ab3c37ec0 100644 --- a/tests/command.positionalOptions.test.js +++ b/tests/command.positionalOptions.test.js @@ -443,3 +443,73 @@ describe('program with allowUnknownOption', () => { expect(program.args).toEqual(['--unknown']); }); }); + +// ------------------------------------------------------------------------------ + +describe('passThroughOptions(xxx) and option after command-argument', () => { + function makeProgram() { + const program = new commander.Command(); + program + .option('-d, --debug') + .arguments(''); + return program; + } + + test('when passThroughOptions() then option passed through', () => { + const program = makeProgram(); + program.passThroughOptions(); + program.parse(['foo', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['foo', '--debug']); + }); + + test('when passThroughOptions(true) then option passed through', () => { + const program = makeProgram(); + program.passThroughOptions(true); + program.parse(['foo', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['foo', '--debug']); + }); + + test('when passThroughOptions(false) then option parsed', () => { + const program = makeProgram(); + program.passThroughOptions(false); + program.parse(['foo', '--debug'], { from: 'user' }); + expect(program.args).toEqual(['foo']); + expect(program.opts().debug).toEqual(true); + }); +}); + +// ------------------------------------------------------------------------------ + +describe('enablePositionalOptions(xxx) and shared option after subcommand', () => { + function makeProgram() { + const program = new commander.Command(); + program + .option('-d, --debug'); + const sub = program + .command('sub') + .option('-d, --debug'); + return { program, sub }; + } + + test('when enablePositionalOptions() then option parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.enablePositionalOptions(); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toEqual(true); + }); + + test('when enablePositionalOptions(true) then option parsed by subcommand', () => { + const { program, sub } = makeProgram(); + program.enablePositionalOptions(true); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toEqual(true); + }); + + test('when enablePositionalOptions(false) then option parsed by program', () => { + const { program, sub } = makeProgram(); + program.enablePositionalOptions(false); + program.parse(['sub', '--debug'], { from: 'user' }); + expect(sub.opts().debug).toBeUndefined(); + expect(program.opts().debug).toEqual(true); + }); +}); From 7ab4d6ddc43dacce5bbabaaf028064ef98a9b157 Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 3 Jan 2021 11:54:43 +1300 Subject: [PATCH 24/26] Fix typo --- tests/command.chain.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/command.chain.test.js b/tests/command.chain.test.js index 4987d8c8b..4cadbd074 100644 --- a/tests/command.chain.test.js +++ b/tests/command.chain.test.js @@ -148,7 +148,7 @@ describe('Command methods that should return this for chaining', () => { expect(result).toBe(program); }); - test('when call .enablePositionOptions() then returns this', () => { + test('when call .enablePositionalOptions() then returns this', () => { const program = new Command(); const result = program.enablePositionalOptions(); expect(result).toBe(program); From 71d92e1b6d9bf89a3015d6ec9a70ce231f34881c Mon Sep 17 00:00:00 2001 From: John Gee Date: Sun, 3 Jan 2021 13:35:32 +1300 Subject: [PATCH 25/26] Add new section to README with parsing configuration. --- Readme.md | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 476700e4f..a45930840 100644 --- a/Readme.md +++ b/Readme.md @@ -35,6 +35,7 @@ Read this in other languages: English | [简体中文](./Readme_zh-CN.md) - [Custom event listeners](#custom-event-listeners) - [Bits and pieces](#bits-and-pieces) - [.parse() and .parseAsync()](#parse-and-parseasync) + - [Parsing Configuration](#parsing-configuration) - [Legacy options as properties](#legacy-options-as-properties) - [TypeScript](#typescript) - [createCommand()](#createcommand) @@ -84,7 +85,7 @@ For example `-a -b -p 80` may be written as `-ab -p80` or even `-abp80`. You can use `--` to indicate the end of the options, and any remaining arguments will be used without being interpreted. -Options on the command line are not positional, and can be specified before or after other arguments. +By default options on the command line are not positional, and can be specified before or after other arguments. ### Common option types, boolean and value @@ -684,6 +685,39 @@ program.parse(); // Implicit, and auto-detect electron program.parse(['-f', 'filename'], { from: 'user' }); ``` +### Parsing Configuration + +If the default parsing does not suit your needs, there are some behaviours to support other usage patterns. + +By default program options are recognised before and after subcommands. To only look for program options before subcommands, use `.enablePositionalOptions()`. This lets you use +an option for a different purpose in subcommands. + +Example file: [positional-options.js](./examples/positional-options.js) + +With positional options, these two lines are different: + +```sh +program -b subcommand +program subcommand -b +``` + +By default options are recognised before and after command-arguments. To only process options that come +before the command-arguments, use `.passThroughOptions()`. This lets you pass the arguments and following options through to another program +without needing to use `--` to end the option processing. +To use pass through options in a subcommand, the programs need to enable positional options. + +Example file: [pass-through-options.js](./examples/pass-through-options.js) + +With pass through options, these two lines are different: + +```sh +program --port=80 arg +program arg --port=80 +``` + + +By default the option processing shows an error for an unknown option. To have an unknown option treated as an ordinary command-argument and continue looking for options, use `.allowUnknownOption()`. This lets you mix known and unknown options. + ### Legacy options as properties Before Commander 7, the option values were stored as properties on the command. From 6f7ffcbdf33920b326c60a6fb45e393822e9f339 Mon Sep 17 00:00:00 2001 From: John Gee Date: Mon, 4 Jan 2021 09:14:25 +1300 Subject: [PATCH 26/26] Tweak wording in README --- Readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Readme.md b/Readme.md index a45930840..4d2d98177 100644 --- a/Readme.md +++ b/Readme.md @@ -694,7 +694,7 @@ an option for a different purpose in subcommands. Example file: [positional-options.js](./examples/positional-options.js) -With positional options, these two lines are different: +With positional options, the `-b` is a program option in the first line and a subcommand option in the second: ```sh program -b subcommand @@ -704,11 +704,11 @@ program subcommand -b By default options are recognised before and after command-arguments. To only process options that come before the command-arguments, use `.passThroughOptions()`. This lets you pass the arguments and following options through to another program without needing to use `--` to end the option processing. -To use pass through options in a subcommand, the programs need to enable positional options. +To use pass through options in a subcommand, the program needs to enable positional options. Example file: [pass-through-options.js](./examples/pass-through-options.js) -With pass through options, these two lines are different: +With pass through options, the `--port=80` is a program option in the line and passed through as a command-argument in the second: ```sh program --port=80 arg